From 25da30369357d4a0125df67c01d5daf743c2aeeb Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 26 Apr 2026 10:42:33 +0700 Subject: [PATCH 001/154] Show forwarded info for forwarded audio files --- Telegram/SourceFiles/data/data_media_types.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 60947ca72d..54a66fc16e 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1360,7 +1360,7 @@ bool MediaFile::forwardedBecomesUnread() const { } bool MediaFile::dropForwardedInfo() const { - return _document->isSong(); + return false; } bool MediaFile::hasSpoiler() const { From e019851344e73a0ab607ababbbf33f6aeee9b36a Mon Sep 17 00:00:00 2001 From: "Sergey A. Osokin" Date: Sat, 25 Apr 2026 22:40:57 -0400 Subject: [PATCH 002/154] Fix warnings. --- Telegram/SourceFiles/boxes/star_gift_box.cpp | 2 -- Telegram/SourceFiles/editor/scene/scene.cpp | 1 - .../SourceFiles/history/view/history_view_element.cpp | 3 --- .../history/view/media/history_view_todo_list.cpp | 11 ----------- .../SourceFiles/payments/ui/payments_reaction_box.cpp | 3 --- Telegram/SourceFiles/ui/boxes/choose_font_box.cpp | 2 -- 6 files changed, 22 deletions(-) diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 6b404b99e1..60ebfd59f3 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -138,11 +138,9 @@ namespace { constexpr auto kPriceTabAll = 0; constexpr auto kPriceTabMy = -1; constexpr auto kPriceTabCollectibles = -2; -constexpr auto kGiftMessageLimit = 255; constexpr auto kSentToastDuration = 3 * crl::time(1000); constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000); -constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000); using namespace HistoryView; diff --git a/Telegram/SourceFiles/editor/scene/scene.cpp b/Telegram/SourceFiles/editor/scene/scene.cpp index 871a20bb92..a4d2bd20de 100644 --- a/Telegram/SourceFiles/editor/scene/scene.cpp +++ b/Telegram/SourceFiles/editor/scene/scene.cpp @@ -111,7 +111,6 @@ constexpr auto kPaddingFactor = 0.4; constexpr auto kMaxWidthFactor = 0.8; constexpr auto kMinWidthFactor = 0.16; constexpr auto kIdealWidthExtra = 2; -constexpr auto kDefaultFontSizeDivisor = 15.; constexpr auto kScaleThreshold = 0.01; class TextEditProxy final : public QGraphicsTextItem { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 1b595c77a8..b94296a470 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -126,9 +126,6 @@ private: friend inline constexpr auto operator<=>( CacheKey, CacheKey) = default; - friend inline constexpr bool operator==( - CacheKey, - CacheKey) = default; }; mutable base::flat_map _cachedBg; mutable base::flat_map _cachedOutline; diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp index 82bc08c639..2cea736d6d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -47,17 +47,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_window.h" namespace HistoryView { -namespace { - -constexpr auto kShowRecentVotersCount = 3; -constexpr auto kRotateSegments = 8; -constexpr auto kRotateAmplitude = 3.; -constexpr auto kScaleSegments = 2; -constexpr auto kScaleAmplitude = 0.03; -constexpr auto kLargestRadialDuration = 30 * crl::time(1000); -constexpr auto kCriticalCloseDuration = 5 * crl::time(1000); - -} // namespace struct TodoList::Task { Task(); diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 3c3b5358d6..4288e55e3e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -60,9 +60,6 @@ struct TopReactorKey { friend inline auto operator<=>( const TopReactorKey &, const TopReactorKey &) = default; - friend inline bool operator==( - const TopReactorKey &, - const TopReactorKey &) = default; }; [[nodiscard]] QImage GenerateBadgeImage( diff --git a/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp b/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp index aa75057019..c298caeaee 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp @@ -32,8 +32,6 @@ namespace Ui { namespace { constexpr auto kMinTextWidth = 120; -constexpr auto kMaxTextWidth = 320; -constexpr auto kMaxTextLines = 3; struct PreviewRequest { QString family; From fc59461e637cabc3e184550ba12e5da85c69509b Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 25 Apr 2026 00:53:57 +0700 Subject: [PATCH 003/154] Show login errors as is. --- Telegram/SourceFiles/intro/intro_code.cpp | 24 ++++++------------- Telegram/SourceFiles/intro/intro_email.cpp | 2 +- .../intro/intro_password_check.cpp | 16 ++++--------- Telegram/SourceFiles/intro/intro_phone.cpp | 6 ++--- Telegram/SourceFiles/intro/intro_signup.cpp | 8 ++----- Telegram/SourceFiles/intro/intro_widget.cpp | 4 ++-- 6 files changed, 18 insertions(+), 42 deletions(-) diff --git a/Telegram/SourceFiles/intro/intro_code.cpp b/Telegram/SourceFiles/intro/intro_code.cpp index 5521275862..cae53b1ed2 100644 --- a/Telegram/SourceFiles/intro/intro_code.cpp +++ b/Telegram/SourceFiles/intro/intro_code.cpp @@ -292,10 +292,8 @@ void CodeWidget::codeSubmitFail(const MTP::Error &error) { }).fail([=](const MTP::Error &error) { codeSubmitFail(error); }).handleFloodErrors().send(); - } else if (Logs::DebugEnabled()) { // internal server error - showCodeError(rpl::single(err + ": " + error.description())); - } else { - showCodeError(rpl::single(Lang::Hard::ServerError())); + } else if (!MTP::IgnoreError(error)) { + showCodeError(rpl::single(err)); } } @@ -479,20 +477,12 @@ void CodeWidget::noTelegramCodeDone(const MTPauth_SentCode &result) { } void CodeWidget::noTelegramCodeFail(const MTP::Error &error) { - if (MTP::IsFloodError(error)) { - _noTelegramCodeRequestId = 0; - showCodeError(tr::lng_flood_error()); - return; - } else if (error.type() == u"SEND_CODE_UNAVAILABLE"_q) { - _noTelegramCodeRequestId = 0; - return; - } - _noTelegramCodeRequestId = 0; - if (Logs::DebugEnabled()) { // internal server error - showCodeError(rpl::single(error.type() + ": " + error.description())); - } else { - showCodeError(rpl::single(Lang::Hard::ServerError())); + if (MTP::IsFloodError(error)) { + showCodeError(tr::lng_flood_error()); + } else if (error.type() != u"SEND_CODE_UNAVAILABLE"_q + && !MTP::IgnoreError(error)) { + showCodeError(rpl::single(error.type())); } } diff --git a/Telegram/SourceFiles/intro/intro_email.cpp b/Telegram/SourceFiles/intro/intro_email.cpp index ecc8d7d636..6f0c88a8e9 100644 --- a/Telegram/SourceFiles/intro/intro_email.cpp +++ b/Telegram/SourceFiles/intro/intro_email.cpp @@ -118,7 +118,7 @@ EmailWidget::EmailWidget( // Show box? error->setText(Lang::Hard::EmailConfirmationExpired()); } else { - error->setText(Lang::Hard::ServerError()); + error->setText(type); } }; diff --git a/Telegram/SourceFiles/intro/intro_password_check.cpp b/Telegram/SourceFiles/intro/intro_password_check.cpp index 47bc74b5ba..2b98ad8961 100644 --- a/Telegram/SourceFiles/intro/intro_password_check.cpp +++ b/Telegram/SourceFiles/intro/intro_password_check.cpp @@ -156,12 +156,8 @@ void PasswordCheckWidget::pwdSubmitFail(const MTP::Error &error) { goBack(); } else if (type == u"SRP_ID_INVALID"_q) { handleSrpIdInvalid(); - } else { - if (Logs::DebugEnabled()) { // internal server error - showError(rpl::single(type + ": " + error.description())); - } else { - showError(rpl::single(Lang::Hard::ServerError())); - } + } else if (!MTP::IgnoreError(error)) { + showError(rpl::single(type)); _pwdField->setFocus(); } } @@ -264,12 +260,8 @@ void PasswordCheckWidget::codeSubmitFail(const MTP::Error &error) { showError(tr::lng_signin_wrong_code()); _codeField->selectAll(); _codeField->showError(); - } else { - if (Logs::DebugEnabled()) { // internal server error - showError(rpl::single(type + ": " + error.description())); - } else { - showError(rpl::single(Lang::Hard::ServerError())); - } + } else if (!MTP::IgnoreError(error)) { + showError(rpl::single(type)); _codeField->setFocus(); } } diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 3f21283037..ace6c87b83 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -281,10 +281,8 @@ void PhoneWidget::phoneSubmitFail(const MTP::Error &error) { showPhoneError(tr::lng_bad_phone()); } else if (err == u"PHONE_NUMBER_BANNED"_q) { Ui::ShowPhoneBannedError(getData()->controller, _sentPhone); - } else if (Logs::DebugEnabled()) { // internal server error - showPhoneError(rpl::single(err + ": " + error.description())); - } else { - showPhoneError(rpl::single(Lang::Hard::ServerError())); + } else if (!MTP::IgnoreError(error)) { + showPhoneError(rpl::single(err)); } } diff --git a/Telegram/SourceFiles/intro/intro_signup.cpp b/Telegram/SourceFiles/intro/intro_signup.cpp index 9261e4ceed..96335c963f 100644 --- a/Telegram/SourceFiles/intro/intro_signup.cpp +++ b/Telegram/SourceFiles/intro/intro_signup.cpp @@ -141,12 +141,8 @@ void SignupWidget::nameSubmitFail(const MTP::Error &error) { } else if (err == "LASTNAME_INVALID") { showError(tr::lng_bad_name()); _last->setFocus(); - } else { - if (Logs::DebugEnabled()) { // internal server error - showError(rpl::single(err + ": " + error.description())); - } else { - showError(rpl::single(Lang::Hard::ServerError())); - } + } else if (!MTP::IgnoreError(error)) { + showError(rpl::single(err)); if (_invertOrder) { _last->setFocus(); } else { diff --git a/Telegram/SourceFiles/intro/intro_widget.cpp b/Telegram/SourceFiles/intro/intro_widget.cpp index 7b2448b258..6f63ef6f16 100644 --- a/Telegram/SourceFiles/intro/intro_widget.cpp +++ b/Telegram/SourceFiles/intro/intro_widget.cpp @@ -614,9 +614,9 @@ void Widget::resetAccount() { } else if (type == u"2FA_RECENT_CONFIRM"_q) { Ui::show(Ui::MakeInformBox( tr::lng_signin_reset_cancelled())); - } else { + } else if (!MTP::IgnoreError(error)) { getData()->controller->hideLayer(); - getStep()->showError(rpl::single(Lang::Hard::ServerError())); + getStep()->showError(rpl::single(type)); } }).send(); }); From 4481a3085537cacf958ba29b9b24eef23a3692cc Mon Sep 17 00:00:00 2001 From: cumdev1337 Date: Tue, 28 Apr 2026 12:39:28 +0300 Subject: [PATCH 004/154] fix: use original stream's duration attribute for transcodes --- .../media/view/media_view_overlay_widget.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 83b2430ca6..82afd73c98 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -5136,16 +5136,12 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { } const auto overrideDuration = _stories || (_chosenQuality && _chosenQuality != _document); - const auto durationDocument = (_chosenQuality && _chosenQuality != _document) - ? _chosenQuality - : _document; - auto options = Streaming::PlaybackOptions{ .position = position, .durationOverride = ((overrideDuration - && durationDocument - && durationDocument->hasDuration()) - ? durationDocument->duration() + && _document + && _document->hasDuration()) + ? _document->duration() : crl::time(0)), .hwAllowed = Core::App().settings().hardwareAcceleratedVideo(), .seekable = !_stories, From d4aed31e6b975524595a5a58f22cebb0a65b629a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 25 Apr 2026 09:15:28 +0300 Subject: [PATCH 005/154] Fixed crash on top button menu close in sticker set box. --- .../SourceFiles/boxes/sticker_set_box.cpp | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 7598c4b650..64a8f65515 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -943,10 +943,12 @@ void StickerSetBox::updateButtons() { raw->setForcedOrigin( Ui::PanelAnimation::Origin::TopRight); top->setForceRippled(true); - raw->setDestroyedCallback([=] { - if (const auto strong = top.data()) { - strong->setForceRippled(false); - } + raw->setDestroyedCallback([top] { + crl::on_main(top, [top] { + if (const auto strong = top.data()) { + strong->setForceRippled(false); + } + }); }); raw->popup(top->mapToGlobal(QPoint( top->width(), @@ -1016,10 +1018,12 @@ void StickerSetBox::updateButtons() { raw->setForcedOrigin( Ui::PanelAnimation::Origin::TopRight); top->setForceRippled(true); - raw->setDestroyedCallback([=] { - if (const auto strong = top.data()) { - strong->setForceRippled(false); - } + raw->setDestroyedCallback([top] { + crl::on_main(top, [top] { + if (const auto strong = top.data()) { + strong->setForceRippled(false); + } + }); }); raw->popup(top->mapToGlobal(QPoint( top->width(), From 7a3e3008d724ed8570010da390b299f1aa4d54d1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 08:16:16 +0300 Subject: [PATCH 006/154] Fixed off-center plus icon in add sticker cell of own sticker set box. --- Telegram/SourceFiles/boxes/sticker_set_box.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 64a8f65515..7200f6431b 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -2469,8 +2469,9 @@ void StickerSetBox::Inner::paintAddCell(QPainter &p) const { ltrRect.width(), ltrRect.height()) : ltrRect; + const auto center = rect::center(rect); const auto inner = QRect( - rect::center(rect) - QPoint( + center - QPoint( st::stickersAddCellBgRadius, st::stickersAddCellBgRadius), Size(st::stickersAddCellBgRadius * 2)); @@ -2486,7 +2487,6 @@ void StickerSetBox::Inner::paintAddCell(QPainter &p) const { const auto plusHalf = st::stickersAddCellPlusSize / 2; const auto thickness = st::stickersAddCellPlusThickness; - const auto center = rect.center(); const auto plusH = QRectF( center.x() - plusHalf, center.y() - thickness / 2., From 91c4b045b8e51dea4f4feea63e87a89c25bf28e4 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 08:16:18 +0300 Subject: [PATCH 007/154] [img-editor] Enabled left-click cropping in sticker creator. --- Telegram/SourceFiles/editor/editor_crop.cpp | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/Telegram/SourceFiles/editor/editor_crop.cpp b/Telegram/SourceFiles/editor/editor_crop.cpp index c68357c41b..ec50507762 100644 --- a/Telegram/SourceFiles/editor/editor_crop.cpp +++ b/Telegram/SourceFiles/editor/editor_crop.cpp @@ -184,10 +184,6 @@ void Crop::paintFrame(QPainter &p) { p.save(); p.setRenderHint(QPainter::Antialiasing, true); p.fillPath(frameShape, st::photoCropPointFg); - if (_data.fixedCrop) { - p.restore(); - return; - } { const auto cornerLength = std::min( float64(st::photoEditorCropPointSize * 2), @@ -297,10 +293,6 @@ void Crop::convertCropPaintToOriginal() { } void Crop::updateEdges() { - if (_data.fixedCrop) { - _edges.clear(); - return; - } const auto &s = _pointSize; const auto &m = _edgePointMargins; const auto &r = _cropPaint; @@ -353,7 +345,7 @@ Qt::Edges Crop::mouseState(const QPoint &p) { } void Crop::mousePressEvent(QMouseEvent *e) { - if (_data.fixedCrop) { + if (_data.fixedCrop && e->button() != Qt::LeftButton) { return; } computeDownState(e->pos()); @@ -363,7 +355,7 @@ void Crop::mousePressEvent(QMouseEvent *e) { } void Crop::mouseReleaseEvent(QMouseEvent *e) { - if (_data.fixedCrop) { + if (_data.fixedCrop && e->button() != Qt::LeftButton) { return; } const auto hadEdge = bool(_down.edge); @@ -495,9 +487,6 @@ void Crop::performMove(const QPoint &pos) { } void Crop::mouseMoveEvent(QMouseEvent *e) { - if (_data.fixedCrop) { - return; - } const auto pos = e->pos(); const auto pressedEdge = _down.edge; @@ -510,6 +499,10 @@ void Crop::mouseMoveEvent(QMouseEvent *e) { update(); } + if (_data.fixedCrop && (e->buttons() & Qt::MiddleButton)) { + return; + } + const auto edge = pressedEdge ? pressedEdge : mouseState(pos); const auto cursor = ((edge == kETL) || (edge == kEBR)) From b2febf52343673d74297058e77c04faf7fa13bb1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 08:24:59 +0300 Subject: [PATCH 008/154] [img-editor] Improved sticker rounded quality by masking after upscale. --- Telegram/SourceFiles/boxes/sticker_creator_box.cpp | 5 ++++- Telegram/SourceFiles/editor/photo_editor_common.cpp | 3 --- Telegram/SourceFiles/editor/photo_editor_common.h | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/boxes/sticker_creator_box.cpp b/Telegram/SourceFiles/boxes/sticker_creator_box.cpp index 6a0c116de9..6c31f2887d 100644 --- a/Telegram/SourceFiles/boxes/sticker_creator_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_creator_box.cpp @@ -155,7 +155,9 @@ void OpenPhotoEditorForSticker( auto applyModifications = [=, done = std::move(onDone)]( const Editor::PhotoModifications &mods) mutable { - auto result = Editor::ImageModified(baseImage->original(), mods); + auto unmasked = mods; + unmasked.cropType = Editor::EditorData::CropType::Rect; + auto result = Editor::ImageModified(baseImage->original(), unmasked); if (result.size() != QSize(kStickerSide, kStickerSide)) { result = result.scaled( kStickerSide, @@ -163,6 +165,7 @@ void OpenPhotoEditorForSticker( Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } + Editor::ApplyShapeMask(result, mods.cropType, mods.cornersLevel); done(std::move(result)); }; diff --git a/Telegram/SourceFiles/editor/photo_editor_common.cpp b/Telegram/SourceFiles/editor/photo_editor_common.cpp index dc5b700913..0a098f98f7 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_common.cpp @@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/userpic_view.h" namespace Editor { -namespace { void ApplyShapeMask( QImage &image, @@ -51,8 +50,6 @@ void ApplyShapeMask( p.drawImage(0, 0, mask); } -} // namespace - float64 RoundedCornersMultiplier(RoundedCornersLevel level) { switch (level) { case RoundedCornersLevel::Large: return Ui::ForumUserpicRadiusMultiplier(); diff --git a/Telegram/SourceFiles/editor/photo_editor_common.h b/Telegram/SourceFiles/editor/photo_editor_common.h index cc98546d99..59354f0fc8 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.h +++ b/Telegram/SourceFiles/editor/photo_editor_common.h @@ -53,4 +53,9 @@ struct PhotoModifications { QImage image, const PhotoModifications &mods); +void ApplyShapeMask( + QImage &image, + EditorData::CropType type, + RoundedCornersLevel cornersLevel); + } // namespace Editor From f02d7cc120dfb5ea27d8c20904d51ece3441fc7c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 09:08:48 +0300 Subject: [PATCH 009/154] [img-editor] Improved visibility of brush size control. --- Telegram/SourceFiles/editor/color_picker.cpp | 3 ++- Telegram/SourceFiles/editor/editor.style | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/editor/color_picker.cpp b/Telegram/SourceFiles/editor/color_picker.cpp index c4f74fe1ba..7e29244eca 100644 --- a/Telegram/SourceFiles/editor/color_picker.cpp +++ b/Telegram/SourceFiles/editor/color_picker.cpp @@ -842,7 +842,8 @@ void ColorPicker::moveSizeControl(const QSize &size) { areaWidth, std::min(areaHeight, size.height())); - const auto collapsedCenterX = sizeControlCurrentCenterX(0.); + const auto collapsedCenterX = sizeControlCurrentCenterX(0.) + - st::photoEditorBrushSizeControlLeftSkip; const auto collapsedLeft = collapsedCenterX - (float64(st::photoEditorBrushSizeControlCollapsedWidth) / 2.); const auto y = (_canvasRect.height() > 0) diff --git a/Telegram/SourceFiles/editor/editor.style b/Telegram/SourceFiles/editor/editor.style index 1d1aede531..f5ede44bc5 100644 --- a/Telegram/SourceFiles/editor/editor.style +++ b/Telegram/SourceFiles/editor/editor.style @@ -154,9 +154,9 @@ photoEditorBlurSizeMultiplier: 3.0; photoEditorBlurPreviewOpacity: 0.25; photoEditorBlurRadius: 20; -photoEditorBrushSizeControlLeftSkip: 0px; +photoEditorBrushSizeControlLeftSkip: -3px; photoEditorBrushSizeControlHeight: 280px; -photoEditorBrushSizeControlCollapsedWidth: 2px; +photoEditorBrushSizeControlCollapsedWidth: 6px; photoEditorBrushSizeControlExpandedTopWidth: 25px; photoEditorBrushSizeControlExpandedBottomWidth: 4px; photoEditorBrushSizeControlExpandShift: 14px; From b21ab20252ceb9f3013d5b6ff19212ecc67a91dd Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 09:32:34 +0300 Subject: [PATCH 010/154] [img-editor] Changed text style cycling to trigger on single click. --- .../editor/scene/scene_item_base.cpp | 6 +- .../editor/scene/scene_item_base.h | 1 + .../editor/scene/scene_item_text.cpp | 93 ++++++++++++++++++- .../editor/scene/scene_item_text.h | 10 ++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/editor/scene/scene_item_base.cpp b/Telegram/SourceFiles/editor/scene/scene_item_base.cpp index 3f29567ad6..4f74a80551 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_base.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_base.cpp @@ -171,7 +171,7 @@ void ItemBase::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { } void ItemBase::mousePressEvent(QGraphicsSceneMouseEvent *event) { - setZValue((*_lastZ)++); + raiseToTop(); if (event->button() == Qt::LeftButton) { _handle = handleType(event->pos()); } @@ -268,6 +268,10 @@ void ItemBase::actionDuplicate() { } } +void ItemBase::raiseToTop() { + setZValue((*_lastZ)++); +} + void ItemBase::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape) { if (const auto s = scene()) { diff --git a/Telegram/SourceFiles/editor/scene/scene_item_base.h b/Telegram/SourceFiles/editor/scene/scene_item_base.h index 3ee271ad2a..1f93b6b0cd 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_base.h +++ b/Telegram/SourceFiles/editor/scene/scene_item_base.h @@ -100,6 +100,7 @@ protected: void actionFlip(); void actionDelete(); void actionDuplicate(); + void raiseToTop(); QRectF contentRect() const; QRectF innerRect() const; diff --git a/Telegram/SourceFiles/editor/scene/scene_item_text.cpp b/Telegram/SourceFiles/editor/scene/scene_item_text.cpp index 834375157a..07fb4d4486 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_text.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_text.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include #include #include #include @@ -39,6 +40,7 @@ constexpr auto kLinePadHFactor = 1. / 3.; constexpr auto kLinePadVFactor = 1. / 8.; constexpr auto kMergeRadiusFactor = 1.5; constexpr auto kLineShiftFactor = 1. / 7.; +constexpr auto kTextStyleClickDelay = crl::time(120); struct LayoutMetrics { int contentWidth = 0; @@ -256,6 +258,18 @@ QPainterPath BuildConnectedBackground( return path; } +TextStyle NextTextStyle(TextStyle style) { + switch (style) { + case TextStyle::Plain: + return TextStyle::Framed; + case TextStyle::Framed: + return TextStyle::SemiTransparent; + case TextStyle::SemiTransparent: + return TextStyle::Plain; + } + Unexpected("Text style in NextTextStyle."); +} + } // namespace QColor EffectiveTextColor(const QColor &color, TextStyle style) { @@ -279,7 +293,11 @@ ItemText::ItemText( , _color(color) , _fontSize(fontSize) , _textStyle(style) -, _imageSize(imageSize) { +, _imageSize(imageSize) +, _textStyleClickTimer([=] { + setTextStyle(NextTextStyle(_textStyle)); + _textStyleClickChanged = true; +}) { renderContent(); } @@ -574,7 +592,80 @@ void ItemText::setTextStyle(TextStyle style) { update(); } +void ItemText::mousePressEvent(QGraphicsSceneMouseEvent *event) { + _textStyleClickTimer.cancel(); + _textStyleClickChanged = false; + _textStyleClickCandidate = (event->button() == Qt::LeftButton) + && contentRect().contains(event->pos()); + _textStyleClickDragging = false; + if (_textStyleClickCandidate) { + _textStyleClickItemPosition = pos(); + _textStyleClickInitialStyle = _textStyle; + event->accept(); + return; + } + ItemBase::mousePressEvent(event); +} + +void ItemText::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { + if (!_textStyleClickCandidate) { + ItemBase::mouseMoveEvent(event); + return; + } + const auto delta = event->screenPos() + - event->buttonDownScreenPos(Qt::LeftButton); + if (!_textStyleClickDragging + && delta.manhattanLength() >= QApplication::startDragDistance()) { + _textStyleClickDragging = true; + if (scene()) { + scene()->clearSelection(); + setSelected(true); + } + raiseToTop(); + setCursor(Qt::ClosedHandCursor); + } + if (_textStyleClickDragging) { + const auto sceneDelta = event->scenePos() + - event->buttonDownScenePos(Qt::LeftButton); + setPos(_textStyleClickItemPosition + sceneDelta); + } + event->accept(); +} + +void ItemText::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { + const auto textStyleClickCandidate = _textStyleClickCandidate; + const auto textStyleClickDragging = _textStyleClickDragging; + _textStyleClickCandidate = false; + _textStyleClickDragging = false; + if (!textStyleClickCandidate) { + ItemBase::mouseReleaseEvent(event); + return; + } + if (textStyleClickDragging) { + unsetCursor(); + event->accept(); + return; + } + if (event->button() == Qt::LeftButton) { + const auto delta = event->screenPos() + - event->buttonDownScreenPos(Qt::LeftButton); + if (delta.manhattanLength() >= QApplication::startDragDistance()) { + event->accept(); + return; + } + _textStyleClickTimer.callOnce(kTextStyleClickDelay); + event->accept(); + } +} + void ItemText::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { + _textStyleClickCandidate = false; + _textStyleClickDragging = false; + _textStyleClickTimer.cancel(); + if (_textStyleClickChanged) { + setTextStyle(_textStyleClickInitialStyle); + _textStyleClickChanged = false; + } if (const auto s = static_cast(scene())) { s->startTextEditing(this); } diff --git a/Telegram/SourceFiles/editor/scene/scene_item_text.h b/Telegram/SourceFiles/editor/scene/scene_item_text.h index a1898b2b3e..050be0c331 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_text.h +++ b/Telegram/SourceFiles/editor/scene/scene_item_text.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/timer.h" #include "base/unique_qptr.h" #include "editor/scene/scene_item_base.h" @@ -65,6 +66,9 @@ public: void restore(SaveState state) override; protected: + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; void performFlip() override; @@ -80,6 +84,12 @@ private: QSize _imageSize; QPixmap _pixmap; base::unique_qptr _contextMenu; + QPointF _textStyleClickItemPosition; + TextStyle _textStyleClickInitialStyle = TextStyle::Plain; + base::Timer _textStyleClickTimer; + bool _textStyleClickCandidate = false; + bool _textStyleClickDragging = false; + bool _textStyleClickChanged = false; struct SavedText { QString text; From 812eddb807bbbd151f35521aa30e428bddadff4a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 09:39:41 +0300 Subject: [PATCH 011/154] [img-editor] Fixed text orientation after rotating media. --- Telegram/SourceFiles/editor/editor_paint.cpp | 2 +- Telegram/SourceFiles/editor/scene/scene.cpp | 12 ++++++++---- Telegram/SourceFiles/editor/scene/scene.h | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/editor/editor_paint.cpp b/Telegram/SourceFiles/editor/editor_paint.cpp index 15577df5c8..0307eea871 100644 --- a/Telegram/SourceFiles/editor/editor_paint.cpp +++ b/Telegram/SourceFiles/editor/editor_paint.cpp @@ -311,7 +311,7 @@ void Paint::applyBrush(const Brush &brush) { } void Paint::createTextItem() { - _scene->createTextAtCenter(); + _scene->createTextAtCenter(-_transform.angle); } void Paint::clearSelection() { diff --git a/Telegram/SourceFiles/editor/scene/scene.cpp b/Telegram/SourceFiles/editor/scene/scene.cpp index a4d2bd20de..c0f03e2369 100644 --- a/Telegram/SourceFiles/editor/scene/scene.cpp +++ b/Telegram/SourceFiles/editor/scene/scene.cpp @@ -584,7 +584,7 @@ void Scene::setupTextProxy( } } -void Scene::createTextAtCenter() { +void Scene::createTextAtCenter(int rotation) { if (_textEdit.proxy) { return; } @@ -626,9 +626,12 @@ void Scene::createTextAtCenter() { minTextWidth, maxTextWidth); proxy->setTextWidth(width); - proxy->setPos(sceneCenter.x() - width / 2., sceneCenter.y()); + const auto anchor = QPointF(width / 2., 0.); + proxy->setTransformOriginPoint(anchor); + proxy->setPos(sceneCenter - anchor); }; adjustWidth(); + proxy->setRotation(rotation); QObject::connect(emojiDoc, &QTextDocument::contentsChanged, [=] { ReplaceEmoji(emojiDoc); @@ -758,8 +761,8 @@ void Scene::finishTextEditing(bool save) { ? RecoverTextFromDocument(_textEdit.proxy->document()).trimmed() : QString(); const auto proxyRect = _textEdit.proxy->boundingRect(); - const auto proxyCenter = _textEdit.proxy->pos() - + QPointF(proxyRect.width() / 2., proxyRect.height() / 2.); + const auto proxyCenter = _textEdit.proxy->mapToScene(proxyRect.center()); + const auto proxyRotation = int(_textEdit.proxy->rotation()); const auto lockedItem = _textEdit.item.lock(); auto *existingItem = lockedItem ? static_cast(lockedItem.get()) @@ -798,6 +801,7 @@ void Scene::finishTextEditing(bool save) { .size = size, .x = int(proxyCenter.x()), .y = int(proxyCenter.y()), + .rotation = proxyRotation, .imageSize = imageSize, }; auto item = std::make_shared( diff --git a/Telegram/SourceFiles/editor/scene/scene.h b/Telegram/SourceFiles/editor/scene/scene.h index 5b4a31657e..bb8608ee47 100644 --- a/Telegram/SourceFiles/editor/scene/scene.h +++ b/Telegram/SourceFiles/editor/scene/scene.h @@ -50,7 +50,7 @@ public: void cancelDrawing(); void startTextEditing(ItemText *item); - void createTextAtCenter(); + void createTextAtCenter(int rotation); void setTextColor(const QColor &color); void setSelectedTextColor(const QColor &color); From 35fa80d44622b445cc6f4d73af32b0f5a97d5806 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 09:54:53 +0300 Subject: [PATCH 012/154] [img-editor] Fixed crash when closing editor with active text tool. --- Telegram/SourceFiles/editor/editor_paint.cpp | 1 + Telegram/SourceFiles/editor/scene/scene.cpp | 26 ++++++++++---------- Telegram/SourceFiles/editor/scene/scene.h | 5 ++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Telegram/SourceFiles/editor/editor_paint.cpp b/Telegram/SourceFiles/editor/editor_paint.cpp index 0307eea871..1056a95b93 100644 --- a/Telegram/SourceFiles/editor/editor_paint.cpp +++ b/Telegram/SourceFiles/editor/editor_paint.cpp @@ -216,6 +216,7 @@ QPointF Paint::mapWidgetDeltaToScene(QPoint delta) const { } Paint::~Paint() { + _scene->cancelTextEditing(); if (_viewport) { _viewport->removeEventFilter(this); } diff --git a/Telegram/SourceFiles/editor/scene/scene.cpp b/Telegram/SourceFiles/editor/scene/scene.cpp index c0f03e2369..127f21ef0d 100644 --- a/Telegram/SourceFiles/editor/scene/scene.cpp +++ b/Telegram/SourceFiles/editor/scene/scene.cpp @@ -318,6 +318,12 @@ void Scene::cancelDrawing() { _canvas->cancelDrawing(); } +void Scene::cancelTextEditing() { + if (_textEdit.proxy) { + finishTextEditing(false, false); + } +} + void Scene::addItem(ItemPtr item) { if (!item) { return; @@ -553,12 +559,14 @@ void Scene::restore(SaveState state) { cancelDrawing(); } -void Scene::setTextEditing(bool editing) { +void Scene::setTextEditing(bool editing, bool notify) { if (_textEditing == editing) { return; } _textEditing = editing; - _textEditStates.fire_copy(editing); + if (notify) { + _textEditStates.fire_copy(editing); + } } void Scene::setupTextProxy( @@ -752,7 +760,7 @@ void Scene::startTextEditing(ItemText *item) { _textColorRequests.fire_copy(item->color()); } -void Scene::finishTextEditing(bool save) { +void Scene::finishTextEditing(bool save, bool notify) { if (!_textEdit.proxy) { return; } @@ -774,7 +782,7 @@ void Scene::finishTextEditing(bool save) { QGraphicsScene::removeItem(_textEdit.proxy.get()); _textEdit.proxy = nullptr; _textEdit.item.reset(); - setTextEditing(false); + setTextEditing(false, notify); const auto defaultStyle = static_cast(_textStyle); @@ -824,15 +832,7 @@ void Scene::finishTextEditing(bool save) { Scene::~Scene() { disconnect(this, &QGraphicsScene::selectionChanged, nullptr, nullptr); - if (_textEdit.proxy) { - setTextEditing(false); - const auto raw = static_cast( - _textEdit.proxy.get()); - raw->onFinish = nullptr; - raw->onCancel = nullptr; - QGraphicsScene::removeItem(_textEdit.proxy.get()); - _textEdit.proxy = nullptr; - } + cancelTextEditing(); QGraphicsScene::removeItem(_canvas.get()); for (const auto &item : items()) { QGraphicsScene::removeItem(item.get()); diff --git a/Telegram/SourceFiles/editor/scene/scene.h b/Telegram/SourceFiles/editor/scene/scene.h index bb8608ee47..252f4795ec 100644 --- a/Telegram/SourceFiles/editor/scene/scene.h +++ b/Telegram/SourceFiles/editor/scene/scene.h @@ -48,6 +48,7 @@ public: void updateZoom(float64 zoom); void cancelDrawing(); + void cancelTextEditing(); void startTextEditing(ItemText *item); void createTextAtCenter(int rotation); @@ -75,8 +76,8 @@ protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; private: void removeIf(Fn proj); - void finishTextEditing(bool save); - void setTextEditing(bool editing); + void finishTextEditing(bool save, bool notify = true); + void setTextEditing(bool editing, bool notify = true); void setupTextProxy( QGraphicsTextItem *proxy, const QColor &color, From 3e68b8e773d2e6f9e0ad521acf8379365b31b9bc Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 10:20:19 +0300 Subject: [PATCH 013/154] Fixed closing of IP warning box before checking proxy in connection box. --- Telegram/SourceFiles/boxes/connection_box.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index fd0be2317a..765a6115fc 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -1828,10 +1828,11 @@ void ProxiesBoxController::ShowApplyConfirmation( } else { box->uiShow()->showBox(Ui::MakeConfirmBox({ .text = tr::lng_proxy_check_ip_warning(), - .confirmed = [=] { + .confirmed = [=](Fn close) { auto &proxy = Core::App().settings().proxy(); proxy.setCheckIpWarningShown(true); Local::writeSettings(); + close(); runCheck(); }, .confirmText = tr::lng_proxy_check_ip_proceed(), From 43919ef77ca9ec1b05e71b1da071b06765bc2378 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 10:52:38 +0300 Subject: [PATCH 014/154] Added search pack shortcuts to stickers list widget. --- Telegram/Resources/langs/lang.strings | 1 + .../chat_helpers/chat_helpers.style | 28 + .../chat_helpers/stickers_list_widget.cpp | 489 ++++++++++++++++-- .../chat_helpers/stickers_list_widget.h | 50 +- 4 files changed, 519 insertions(+), 49 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f4397be36d..51d64ca353 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4482,6 +4482,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_masks_count#other" = "{count} masks"; "lng_custom_emoji_count#one" = "{count} emoji"; "lng_custom_emoji_count#other" = "{count} emoji"; +"lng_search_back_to_results" = "Back to search"; "lng_stickers_attached_sets" = "Sets of attached stickers"; "lng_custom_emoji_used_sets" = "Sets of used emoji"; "lng_custom_emoji_remove_pack_button" = "Remove Emoji"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 9df1cf2c8e..ef39a0a925 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -122,6 +122,20 @@ EmojiPan { tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; + searchPacksTop: pixels; + searchPackWidth: pixels; + searchPackHeight: pixels; + searchPackSkip: pixels; + searchPackIconSize: pixels; + searchPackIconTop: pixels; + searchPackTextTop: pixels; + searchPackTextPadding: pixels; + searchPacksBottom: pixels; + searchBackHeight: pixels; + searchBackIconLeft: pixels; + searchBackIconTop: pixels; + searchBackTextLeft: pixels; + searchBackTextTop: pixels; colorAll: IconButton; colorAllLabel: FlatLabel; removeSet: IconButton; @@ -769,6 +783,20 @@ defaultEmojiPan: EmojiPan { tabs: emojiTabs; search: defaultTabbedSearch; searchMargin: margins(1px, 11px, 2px, 5px); + searchPacksTop: 8px; + searchPackWidth: 70px; + searchPackHeight: 78px; + searchPackSkip: 6px; + searchPackIconSize: 42px; + searchPackIconTop: 8px; + searchPackTextTop: 55px; + searchPackTextPadding: 5px; + searchPacksBottom: 8px; + searchBackHeight: 40px; + searchBackIconLeft: 5px; + searchBackIconTop: 3px; + searchBackTextLeft: 40px; + searchBackTextTop: 10px; colorAll: emojiPanColorAll; colorAllLabel: emojiPanColorAllLabel; removeSet: stickerPanRemoveSet; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 0843ed7ccd..16ccd46ade 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -498,6 +498,7 @@ template bool StickersListWidget::enumerateSections(Callback callback) const { auto info = SectionInfo(); info.top = _search ? _search->height() : 0; + info.top += searchShortcutsHeight(); const auto &sets = shownSets(); for (auto i = 0; i != sets.size(); ++i) { auto &set = sets[i]; @@ -582,6 +583,7 @@ int StickersListWidget::countDesiredHeight(int newWidth) { - st().margin.left(); _singleSize = QSize(singleWidth, singleWidth); setColumnCount(columnCount); + refreshSearchShortcutsScroll(newWidth); auto visibleHeight = minimalHeight(); auto minimalHeight = (visibleHeight - st::stickerPanPadding); @@ -712,6 +714,9 @@ void StickersListWidget::searchForSets( _searchNextQuery = cleaned; _searchRequestTimer.callOnce(kSearchRequestDelay); } + _searchSelectedSetId = 0; + _searchShortcutsScroll = 0; + _searchShortcutsDragging = false; showSearchResults(); } } @@ -731,6 +736,11 @@ void StickersListWidget::cancelSetsSearch() { _searchSetsCache.clear(); _searchStickersCache.clear(); _searchStickersNextOffset.clear(); + _searchShortcutSets.clear(); + _searchSelectedSetId = 0; + _searchShortcutsScroll = 0; + _searchShortcutsScrollMax = 0; + _searchShortcutsDragging = false; refreshSearchRows(nullptr); } @@ -753,9 +763,11 @@ void StickersListWidget::refreshSearchRows( const auto wasSection = _section; auto wasSets = base::take(_searchSets); + auto wasShortcuts = base::take(_searchShortcutSets); const auto guard = gsl::finally([&] { if (_section == wasSection && _section == Section::Search) { takeHeavyData(_searchSets, wasSets); + takeHeavyData(_searchShortcutSets, wasShortcuts); } }); @@ -764,14 +776,16 @@ void StickersListWidget::refreshSearchRows( && (foundStickersIt != _searchStickersCache.end()) && !foundStickersIt->second.empty(); - fillFilteredStickersRow(); - if (!_isEffects) { - fillLocalSearchRows(_searchNextQuery); + refreshSearchShortcuts(_searchNextQuery, cloudSets); } - - if (hasCloudFoundStickers) { - fillFoundStickersRow(foundStickersIt->second); + if (searchShortcutSelected()) { + fillSelectedSearchShortcut(); + } else { + fillFilteredStickersRow(); + if (hasCloudFoundStickers) { + fillFoundStickersRow(foundStickersIt->second); + } } if (!cloudSets && _searchNextQuery.isEmpty()) { showStickerSet(!_mySets.empty() @@ -781,9 +795,6 @@ void StickersListWidget::refreshSearchRows( } setSection(Section::Search); - if (!_isEffects && cloudSets) { - fillCloudSearchRows(*cloudSets); - } refreshIcons(ValidateIconAnimations::Scroll); _lastMousePosition = QCursor::pos(); @@ -796,12 +807,34 @@ rpl::producer StickersListWidget::recentShownCount() const { return _recentShownCount.value(); } -void StickersListWidget::fillLocalSearchRows(const QString &query) { +void StickersListWidget::refreshSearchShortcuts( + const QString &query, + const std::vector *cloudSets) { + fillLocalSearchShortcuts(query); + if (cloudSets) { + const auto &sets = session().data().stickers().sets(); + for (const auto setId : *cloudSets) { + if (const auto it = sets.find(setId); it != sets.end()) { + addSearchShortcut(it->second.get()); + } + } + } + if (_searchSelectedSetId + && !ranges::contains( + _searchShortcutSets, + _searchSelectedSetId, + &Set::id)) { + _searchSelectedSetId = 0; + } + refreshSearchShortcutsScroll(width()); +} + +void StickersListWidget::fillLocalSearchShortcuts(const QString &query) { const auto searchWordsList = TextUtilities::PrepareSearchWords(query); if (searchWordsList.isEmpty()) { return; } - auto searchWordInTitle = []( + const auto searchWordInTitle = []( const QStringList &titleWords, const QString &searchWord) { for (const auto &titleWord : titleWords) { @@ -811,7 +844,7 @@ void StickersListWidget::fillLocalSearchRows(const QString &query) { } return false; }; - auto allSearchWordsInTitle = [&]( + const auto allSearchWordsInTitle = [&]( const QStringList &titleWords) { for (const auto &searchWord : searchWordsList) { if (!searchWordInTitle(titleWords, searchWord)) { @@ -823,22 +856,155 @@ void StickersListWidget::fillLocalSearchRows(const QString &query) { const auto &sets = session().data().stickers().sets(); for (const auto &[setId, titleWords] : _searchIndex) { - if (allSearchWordsInTitle(titleWords)) { - if (const auto it = sets.find(setId); it != sets.end()) { - addSearchRow(it->second.get()); - } + if (!allSearchWordsInTitle(titleWords)) { + continue; + } else if (const auto it = sets.find(setId); it != sets.end()) { + addSearchShortcut(it->second.get()); } } } -void StickersListWidget::fillCloudSearchRows( - const std::vector &cloudSets) { - const auto &sets = session().data().stickers().sets(); - for (const auto setId : cloudSets) { - if (const auto it = sets.find(setId); it != sets.end()) { - addSearchRow(it->second.get()); - } +bool StickersListWidget::addSearchShortcut(not_null set) { + if (ranges::contains(_searchShortcutSets, set->id, &Set::id)) { + return false; } + const auto skipPremium = !session().premiumPossible(); + auto elements = PrepareStickers( + set->stickers.empty() ? set->covers : set->stickers, + skipPremium); + if (elements.empty()) { + return false; + } + _searchShortcutSets.emplace_back( + set->id, + set, + set->flags, + set->title, + set->shortName, + set->count, + false, + std::move(elements)); + _searchShortcutSets.back().thumbnailDocument + = set->lookupThumbnailDocument(); + return true; +} + +void StickersListWidget::fillSelectedSearchShortcut() { + const auto &sets = session().data().stickers().sets(); + const auto it = sets.find(_searchSelectedSetId); + if (it == sets.end()) { + _searchSelectedSetId = 0; + return; + } + const auto set = it->second.get(); + const auto skipPremium = !session().premiumPossible(); + auto elements = PrepareStickers( + set->stickers.empty() ? set->covers : set->stickers, + skipPremium); + if (elements.empty()) { + _searchSelectedSetId = 0; + return; + } + _searchSets.emplace_back( + set->id, + set, + set->flags | SetFlag::Special, + tr::lng_stickers_count(tr::now, lt_count, set->count), + set->shortName, + set->count, + false, + std::move(elements)); +} + +bool StickersListWidget::searchShortcutsShown() const { + return (_section == Section::Search) && !_searchShortcutSets.empty(); +} + +bool StickersListWidget::searchShortcutSelected() const { + return _searchSelectedSetId != 0; +} + +int StickersListWidget::searchShortcutsTop() const { + return _search ? _search->height() : 0; +} + +int StickersListWidget::searchShortcutsHeight() const { + return searchShortcutsShown() + ? ((searchShortcutSelected() ? st().searchBackHeight : 0) + + st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom) + : 0; +} + +QRect StickersListWidget::searchBackRect() const { + return QRect( + 0, + searchShortcutsTop(), + width(), + searchShortcutSelected() ? st().searchBackHeight : 0); +} + +QRect StickersListWidget::searchShortcutRect(int index) const { + Expects(index >= 0 && index < _searchShortcutSets.size()); + + const auto left = st().headerLeft + - st().margin.left() + - _searchShortcutsScroll + + index * (st().searchPackWidth + st().searchPackSkip); + const auto top = searchShortcutsTop() + + (searchShortcutSelected() ? st().searchBackHeight : 0) + + st().searchPacksTop; + return QRect( + left, + top, + st().searchPackWidth, + st().searchPackHeight); +} + +void StickersListWidget::refreshSearchShortcutsScroll(int newWidth) { + if (_searchShortcutSets.empty()) { + _searchShortcutsScroll = 0; + _searchShortcutsScrollMax = 0; + return; + } + const auto count = int(_searchShortcutSets.size()); + const auto full = st().headerLeft + - st().margin.left() + + count * st().searchPackWidth + + (count - 1) * st().searchPackSkip + + st().margin.right(); + _searchShortcutsScrollMax = std::max(full - newWidth, 0); + scrollSearchShortcutsTo(_searchShortcutsScroll); +} + +void StickersListWidget::scrollSearchShortcutsTo(int value) { + const auto scroll = std::clamp( + value, + 0, + _searchShortcutsScrollMax); + if (_searchShortcutsScroll == scroll) { + return; + } + _searchShortcutsScroll = scroll; + update(0, searchShortcutsTop(), width(), searchShortcutsHeight()); +} + +void StickersListWidget::toggleSearchShortcut(int index) { + if (index < 0 || index >= _searchShortcutSets.size()) { + return; + } + const auto setId = _searchShortcutSets[index].id; + _searchSelectedSetId = (_searchSelectedSetId == setId) ? 0 : setId; + showSearchResults(); +} + +void StickersListWidget::backToSearchResults() { + if (!_searchSelectedSetId) { + return; + } + _searchSelectedSetId = 0; + showSearchResults(); } void StickersListWidget::fillFoundStickersRow( @@ -889,22 +1055,6 @@ void StickersListWidget::fillFilteredStickersRow() { std::move(elements)); } -void StickersListWidget::addSearchRow(not_null set) { - const auto skipPremium = !session().premiumPossible(); - auto elements = PrepareStickers( - set->stickers.empty() ? set->covers : set->stickers, - skipPremium); - _searchSets.emplace_back( - set->id, - set, - set->flags, - set->title, - set->shortName, - set->count, - !SetInMyList(set->flags), - std::move(elements)); -} - void StickersListWidget::toggleSearchLoading(bool loading) { if (_search) { _search->setLoading(loading); @@ -1160,6 +1310,139 @@ void StickersListWidget::paintEvent(QPaintEvent *e) { paintStickers(p, clip); } +void StickersListWidget::paintSearchShortcuts(Painter &p, QRect clip) { + if (!searchShortcutsShown() + || clip.bottom() < searchShortcutsTop() + || clip.top() >= searchShortcutsTop() + searchShortcutsHeight()) { + return; + } + if (searchShortcutSelected()) { + const auto back = searchBackRect(); + const auto selected = std::get_if( + !v::is_null(_pressed) ? &_pressed : &_selected); + const auto &icon = selected + ? st().search.back.iconOver + : st().search.back.icon; + icon.paint( + p, + st().searchBackIconLeft, + back.y() + st().searchBackIconTop, + width()); + const auto text = tr::lng_search_back_to_results(tr::now); + const auto &font = st::emojiPanHeaderFont; + const auto available = width() + - st().searchBackTextLeft + - st().margin.right(); + auto shown = text; + auto textWidth = font->width(shown); + if (textWidth > available) { + shown = font->elided(shown, available); + textWidth = font->width(shown); + } + p.setFont(font); + p.setPen(st().headerFg); + p.drawTextLeft( + st().searchBackTextLeft, + back.y() + st().searchBackTextTop, + width(), + shown, + textWidth); + } + + const auto selectedShortcut = std::get_if( + !v::is_null(_pressed) ? &_pressed : &_selected); + p.save(); + p.setClipRect( + QRect( + 0, + searchShortcutsTop() + + (searchShortcutSelected() ? st().searchBackHeight : 0), + width(), + st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom), + Qt::IntersectClip); + for (auto i = 0, count = int(_searchShortcutSets.size()); i != count; ++i) { + auto &set = _searchShortcutSets[i]; + const auto rect = searchShortcutRect(i); + if (!rect.intersects(clip)) { + continue; + } + const auto selected = (set.id == _searchSelectedSetId) + || (selectedShortcut && selectedShortcut->index == i); + if (selected) { + _overBg.paint(p, myrtlrect(rect)); + } + if (set.ripple) { + set.ripple->paint( + p, + myrtlrect(rect).x(), + rect.y(), + width()); + if (set.ripple->empty()) { + set.ripple.reset(); + } + } + const auto icon = QRect( + rect.x() + (rect.width() - st().searchPackIconSize) / 2, + rect.y() + st().searchPackIconTop, + st().searchPackIconSize, + st().searchPackIconSize); + paintSearchShortcutIcon(p, set, icon); + + const auto available = rect.width() + - 2 * st().searchPackTextPadding; + auto title = set.title; + auto titleWidth = st::normalFont->width(title); + if (titleWidth > available) { + title = st::normalFont->elided(title, available); + titleWidth = st::normalFont->width(title); + } + p.setFont(st::normalFont); + p.setPen(st().textFg); + p.drawTextLeft( + rect.x() + st().searchPackTextPadding, + rect.y() + st().searchPackTextTop, + width(), + title, + titleWidth); + } + p.restore(); +} + +void StickersListWidget::paintSearchShortcutIcon( + Painter &p, + Set &set, + QRect rect) { + if (set.stickers.empty()) { + return; + } + auto &sticker = set.stickers.front(); + sticker.ensureMediaCreated(); + const auto document = sticker.document; + const auto media = sticker.documentMedia.get(); + media->thumbnailWanted(document->stickerSetOrigin()); + media->checkStickerSmall(); + + const auto size = ComputeStickerSize(document, rect.size()); + if (size.isEmpty()) { + return; + } + const auto point = rect.topLeft() + QPoint( + (rect.width() - size.width()) / 2, + (rect.height() - size.height()) / 2); + if (const auto image = media->getStickerSmall()) { + const auto pixmap = image->pixSingle(size, { .outer = size }); + p.drawPixmapLeft(point, width(), pixmap); + } else { + PaintStickerThumbnailPath( + p, + media, + QRect(point, size), + _pathGradient.get()); + } +} + void StickersListWidget::paintStickers(Painter &p, QRect clip) { auto fromColumn = floorclamp( clip.x() - stickersLeft(), @@ -1179,6 +1462,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { _paintAsPremium = session().premium(); _pathGradient->startFrame(0, width(), width() / 2); + paintSearchShortcuts(p, clip); auto &sets = shownSets(); const auto selectedSticker = std::get_if(&_selected); @@ -1189,7 +1473,9 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { const auto now = crl::now(); const auto paused = On(PowerSaving::kStickersPanel) || this->paused(); - if (sets.empty() && _section == Section::Search) { + if (sets.empty() + && _searchShortcutSets.empty() + && _section == Section::Search) { const auto loading = _searchLoading || _searchRequestTimer.isActive(); Inner::paintEmptySearchResults( p, @@ -2000,8 +2286,15 @@ void StickersListWidget::mousePressEvent(QMouseEvent *e) { updateSelected(); setPressed(_selected); + if (std::get_if(&_selected)) { + _searchShortcutsMouseDown = _lastMousePosition; + _searchShortcutsDragStart = _searchShortcutsScroll; + _searchShortcutsDragging = false; + } ClickHandler::pressed(); - _previewTimer.callOnce(QApplication::startDragTime()); + if (std::get_if(&_selected)) { + _previewTimer.callOnce(QApplication::startDragTime()); + } } void StickersListWidget::setPressed(OverState newPressed) { @@ -2012,6 +2305,14 @@ void StickersListWidget::setPressed(OverState newPressed) { if (set.ripple) { set.ripple->lastStop(); } + } else if (auto shortcut = std::get_if(&_pressed)) { + if (shortcut->index >= 0 + && shortcut->index < _searchShortcutSets.size()) { + auto &set = _searchShortcutSets[shortcut->index]; + if (set.ripple) { + set.ripple->lastStop(); + } + } } else if (std::get_if(&_pressed)) { if (_megagroupSetButtonRipple) { _megagroupSetButtonRipple->lastStop(); @@ -2027,6 +2328,16 @@ void StickersListWidget::setPressed(OverState newPressed) { } set.ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section)); + } else if (auto shortcut = std::get_if(&_pressed)) { + if (shortcut->index >= 0 + && shortcut->index < _searchShortcutSets.size()) { + auto &set = _searchShortcutSets[shortcut->index]; + if (!set.ripple) { + set.ripple = createSearchShortcutRipple(shortcut->index); + } + set.ripple->add(mapFromGlobal(QCursor::pos()) + - myrtlrect(searchShortcutRect(shortcut->index)).topLeft()); + } } else if (std::get_if(&_pressed)) { if (!_megagroupSetButtonRipple) { auto mask = Ui::RippleAnimation::RoundRectMask( @@ -2042,6 +2353,26 @@ void StickersListWidget::setPressed(OverState newPressed) { } } +std::unique_ptr +StickersListWidget::createSearchShortcutRipple(int index) { + Expects(index >= 0 && index < _searchShortcutSets.size()); + + const auto setId = _searchShortcutSets[index].id; + auto mask = Ui::RippleAnimation::RoundRectMask( + searchShortcutRect(index).size(), + st::roundRadiusLarge); + return std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [this, setId] { + const auto i = ranges::find(_searchShortcutSets, setId, &Set::id); + if (i != _searchShortcutSets.end()) { + rtlupdate(searchShortcutRect( + int(i - _searchShortcutSets.begin()))); + } + }); +} + QRect StickersListWidget::megagroupSetButtonRectFinal() const { auto result = QRect(); if (_section == Section::Stickers) { @@ -2236,10 +2567,20 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { _lastMousePosition = e->globalPos(); updateSelected(); + if (_searchShortcutsDragging) { + _searchShortcutsDragging = false; + return; + } auto &sets = shownSets(); if (!v::is_null(pressed) && pressed == _selected) { - if (auto sticker = std::get_if(&pressed)) { + if (std::get_if(&pressed)) { + backToSearchResults(); + return; + } else if (auto shortcut = std::get_if(&pressed)) { + toggleSearchShortcut(shortcut->index); + return; + } else if (auto sticker = std::get_if(&pressed)) { Assert(sticker->section >= 0 && sticker->section < sets.size()); auto &set = sets[sticker->section]; Assert(sticker->index >= 0 && sticker->index < set.stickers.size()); @@ -2356,6 +2697,20 @@ void StickersListWidget::setColumnCount(int count) { void StickersListWidget::mouseMoveEvent(QMouseEvent *e) { _lastMousePosition = e->globalPos(); + if (std::get_if(&_pressed) + && _searchShortcutsScrollMax > 0) { + const auto delta = _lastMousePosition - _searchShortcutsMouseDown; + if (!_searchShortcutsDragging + && delta.manhattanLength() >= QApplication::startDragDistance()) { + _searchShortcutsDragging = true; + } + if (_searchShortcutsDragging) { + scrollSearchShortcutsTo( + _searchShortcutsDragStart + + (rtl() ? -1 : 1) * -delta.x()); + return; + } + } updateSelected(); } @@ -2427,6 +2782,9 @@ void StickersListWidget::clearHeavyData() { for (auto &set : shownSets()) { clearHeavyIn(set, false); } + for (auto &set : _searchShortcutSets) { + clearHeavyIn(set, false); + } } void StickersListWidget::refreshStickers() { @@ -2518,20 +2876,34 @@ void StickersListWidget::refreshSearchSets() { const auto &sets = session().data().stickers().sets(); const auto skipPremium = !session().premiumPossible(); - for (auto &entry : _searchSets) { + const auto refresh = [&](Set &entry) { if (const auto it = sets.find(entry.id); it != sets.end()) { const auto set = it->second.get(); - entry.flags = set->flags; - auto elements = PrepareStickers(set->stickers, skipPremium); + const auto selected = (_searchSelectedSetId == entry.id); + entry.flags = selected + ? (set->flags | SetFlag::Special) + : set->flags; + auto elements = PrepareStickers( + set->stickers.empty() ? set->covers : set->stickers, + skipPremium); if (!elements.empty()) { entry.lottiePlayer = nullptr; entry.stickers = std::move(elements); } - if (!SetInMyList(entry.flags)) { + entry.thumbnailDocument = set->lookupThumbnailDocument(); + if (selected) { + entry.externalLayout = false; + } else if (!SetInMyList(entry.flags)) { _localSetsManager->removeInstalledLocally(entry.id); entry.externalLayout = true; } } + }; + for (auto &entry : _searchSets) { + refresh(entry); + } + for (auto &entry : _searchShortcutSets) { + refresh(entry); } } @@ -2932,6 +3304,23 @@ void StickersListWidget::updateSelected() { clearSelection(); return; } + if (searchShortcutsShown() + && p.y() >= searchShortcutsTop() + && p.y() < searchShortcutsTop() + searchShortcutsHeight()) { + if (searchShortcutSelected() && searchBackRect().contains(p)) { + newSelected = OverSearchBack{}; + } else { + for (auto i = 0, count = int(_searchShortcutSets.size()); + i != count; ++i) { + if (myrtlrect(searchShortcutRect(i)).contains(p)) { + newSelected = OverSearchShortcut{ i }; + break; + } + } + } + setSelected(newSelected); + return; + } auto &sets = shownSets(); auto sx = (rtl() ? width() - p.x() : p.x()) - stickersLeft(); if (!shownSets().empty()) { @@ -3029,6 +3418,14 @@ void StickersListWidget::setSelected(OverState newSelected) { } else { rtlupdate(removeButtonRect(button->section)); } + } else if (auto shortcut + = std::get_if(&_selected)) { + if (shortcut->index >= 0 + && shortcut->index < _searchShortcutSets.size()) { + rtlupdate(searchShortcutRect(shortcut->index)); + } + } else if (std::get_if(&_selected)) { + rtlupdate(searchBackRect()); } else if (std::get_if(&_selected)) { rtlupdate(megagroupSetButtonRectFinal()); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index f28369fcf9..d6a842eef1 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -199,6 +199,24 @@ private: return !(*this == other); } }; + struct OverSearchShortcut { + int index = 0; + + inline bool operator==(OverSearchShortcut other) const { + return (index == other.index); + } + inline bool operator!=(OverSearchShortcut other) const { + return !(*this == other); + } + }; + struct OverSearchBack { + inline bool operator==(OverSearchBack other) const { + return true; + } + inline bool operator!=(OverSearchBack other) const { + return !(*this == other); + } + }; struct OverGroupAdd { inline bool operator==(OverGroupAdd other) const { return true; @@ -212,6 +230,8 @@ private: OverSticker, OverSet, OverButton, + OverSearchShortcut, + OverSearchBack, OverGroupAdd>; struct SectionInfo { @@ -271,6 +291,8 @@ private: [[nodiscard]] std::unique_ptr createButtonRipple( int section); [[nodiscard]] QPoint buttonRippleTopLeft(int section) const; + [[nodiscard]] std::unique_ptr + createSearchShortcutRipple(int index); [[nodiscard]] std::vector &shownSets(); [[nodiscard]] const std::vector &shownSets() const; @@ -376,11 +398,26 @@ private: void checkPaginateSearchStickers(int visibleTop, int visibleBottom); void refreshSearchRows(); void refreshSearchRows(const std::vector *cloudSets); + void refreshSearchShortcuts( + const QString &query, + const std::vector *cloudSets); + void fillLocalSearchShortcuts(const QString &query); + bool addSearchShortcut(not_null set); + void fillSelectedSearchShortcut(); + [[nodiscard]] bool searchShortcutsShown() const; + [[nodiscard]] bool searchShortcutSelected() const; + [[nodiscard]] int searchShortcutsHeight() const; + [[nodiscard]] int searchShortcutsTop() const; + [[nodiscard]] QRect searchBackRect() const; + [[nodiscard]] QRect searchShortcutRect(int index) const; + void refreshSearchShortcutsScroll(int newWidth); + void scrollSearchShortcutsTo(int value); + void paintSearchShortcuts(Painter &p, QRect clip); + void paintSearchShortcutIcon(Painter &p, Set &set, QRect rect); + void toggleSearchShortcut(int index); + void backToSearchResults(); void fillFilteredStickersRow(); - void fillLocalSearchRows(const QString &query); - void fillCloudSearchRows(const std::vector &cloudSets); void fillFoundStickersRow(const std::vector &stickerIds); - void addSearchRow(not_null set); void toggleSearchLoading(bool loading); void showPreview(); @@ -403,6 +440,7 @@ private: std::vector _mySets; std::vector _officialSets; std::vector _searchSets; + std::vector _searchShortcutSets; int _featuredSetsCount = 0; std::vector _custom; std::vector _cornerEmoji; @@ -467,6 +505,12 @@ private: std::vector> _searchIndex; base::Timer _searchRequestTimer; QString _searchQuery, _searchNextQuery; + uint64 _searchSelectedSetId = 0; + int _searchShortcutsScroll = 0; + int _searchShortcutsScrollMax = 0; + int _searchShortcutsDragStart = 0; + QPoint _searchShortcutsMouseDown; + bool _searchShortcutsDragging = false; mtpRequestId _searchSetsRequestId = 0; mtpRequestId _searchStickersRequestId = 0; bool _searchLoading = false; From 35fe7c084685b8d5cdb80cd6da07890b6b094ac6 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 11:05:32 +0300 Subject: [PATCH 015/154] Added search pack shortcuts to emoji list widget. --- .../chat_helpers/chat_helpers.style | 6 +- .../chat_helpers/emoji_list_widget.cpp | 521 ++++++++++++++++-- .../chat_helpers/emoji_list_widget.h | 50 +- 3 files changed, 518 insertions(+), 59 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index ef39a0a925..fb280209d9 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -783,7 +783,7 @@ defaultEmojiPan: EmojiPan { tabs: emojiTabs; search: defaultTabbedSearch; searchMargin: margins(1px, 11px, 2px, 5px); - searchPacksTop: 8px; + searchPacksTop: 4px; searchPackWidth: 70px; searchPackHeight: 78px; searchPackSkip: 6px; @@ -791,10 +791,10 @@ defaultEmojiPan: EmojiPan { searchPackIconTop: 8px; searchPackTextTop: 55px; searchPackTextPadding: 5px; - searchPacksBottom: 8px; + searchPacksBottom: 4px; searchBackHeight: 40px; searchBackIconLeft: 5px; - searchBackIconTop: 3px; + searchBackIconTop: 6px; searchBackTextLeft: 40px; searchBackTextTop: 10px; colorAll: emojiPanColorAll; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 6a5d6f3693..19fe615572 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/controls/tabbed_search.h" #include "ui/text/format_values.h" +#include "ui/text/text_entity.h" #include "ui/effects/animations.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" @@ -652,6 +653,11 @@ void EmojiListWidget::applyNextSearchQuery() { _searchResults.clear(); _searchCustomIds.clear(); _searchSets.clear(); + _searchShortcutSets.clear(); + _searchSelectedSetId = 0; + _searchShortcutsScroll = 0; + _searchShortcutsScrollMax = 0; + _searchShortcutsDragging = false; } resizeToWidth(width()); _recentShownCount = searching @@ -668,6 +674,9 @@ void EmojiListWidget::applyNextSearchQuery() { finish(false); return; } + _searchSelectedSetId = 0; + _searchShortcutsScroll = 0; + _searchShortcutsDragging = false; const auto guard = gsl::finally([&] { finish(); }); auto plain = collectPlainSearchResults(); _searchEmoticon = QString(); @@ -681,6 +690,7 @@ void EmojiListWidget::applyNextSearchQuery() { _searchResults.clear(); _searchCustomIds.clear(); _searchSets.clear(); + _searchShortcutSets.clear(); if (_mode == Mode::Full) { for (const auto emoji : plain) { _searchResults.push_back({ @@ -708,6 +718,7 @@ void EmojiListWidget::applyNextSearchQuery() { } _searchNextRequestQuery = _searchQueryText; _searchRequestQuery = _searchQueryText; + refreshSearchShortcuts(); const auto cloudCached = _searchCloudCache.find(_searchRequestQuery) != _searchCloudCache.cend(); const auto setsCached = _searchSetsCache.find(_searchRequestQuery) @@ -715,7 +726,6 @@ void EmojiListWidget::applyNextSearchQuery() { if (cloudCached || setsCached) { _searchRequestTimer.cancel(); fillCloudSearchResults(); - fillCloudSearchSets(); if (!cloudCached || !setsCached) { sendSearchRequest(); } @@ -916,6 +926,11 @@ void EmojiListWidget::cancelSearchRequest() { _searchCloudNextOffset.clear(); _searchSetsCache.clear(); _searchSets.clear(); + _searchShortcutSets.clear(); + _searchSelectedSetId = 0; + _searchShortcutsScroll = 0; + _searchShortcutsScrollMax = 0; + _searchShortcutsDragging = false; } void EmojiListWidget::searchCloudResultsDone( @@ -1059,21 +1074,26 @@ void EmojiListWidget::showSearchResults() { _searchResults.clear(); _searchCustomIds.clear(); _searchSets.clear(); + _searchShortcutSets.clear(); _searchEmoji.clear(); - auto plain = collectPlainSearchResults(); - if (_mode == Mode::Full) { - for (const auto emoji : plain) { - _searchResults.push_back({ - .id = { emoji }, - }); + refreshSearchShortcuts(); + if (searchShortcutSelected()) { + fillSelectedSearchShortcut(); + } else { + auto plain = collectPlainSearchResults(); + if (_mode == Mode::Full) { + for (const auto emoji : plain) { + _searchResults.push_back({ + .id = { emoji }, + }); + } } + if (_mode != Mode::Full || session().premium()) { + appendPremiumSearchResults(); + } + fillCloudSearchResults(); } - if (_mode != Mode::Full || session().premium()) { - appendPremiumSearchResults(); - } - fillCloudSearchResults(); - fillCloudSearchSets(); resizeToWidth(width()); _recentShownCount = _searchResults.size(); @@ -1107,55 +1127,224 @@ void EmojiListWidget::fillCloudSearchResults() { } } -void EmojiListWidget::fillCloudSearchSets() { +void EmojiListWidget::refreshSearchShortcuts() { + fillLocalSearchShortcuts(_searchQueryText); const auto it = _searchSetsCache.find(_searchRequestQuery); - if (it == _searchSetsCache.cend() || it->second.empty()) { - return; - } - const auto &sets = session().data().stickers().sets(); - for (const auto setId : it->second) { - const auto setIt = sets.find(setId); - if (setIt == sets.end()) { - continue; - } - const auto set = setIt->second.get(); - const auto &list = set->stickers.empty() - ? set->covers - : set->stickers; - if (list.empty()) { - continue; - } - auto customs = std::vector(); - customs.reserve(list.size()); - for (const auto document : list) { - if (const auto sticker = document->sticker()) { - const auto statusId = EmojiStatusId{ document->id }; - customs.push_back({ - .custom = resolveCustomEmoji( - statusId, - document, - setId), - .document = document, - .emoji = Ui::Emoji::Find(sticker->alt), - }); + if (it != _searchSetsCache.cend()) { + const auto &sets = session().data().stickers().sets(); + for (const auto setId : it->second) { + if (const auto setIt = sets.find(setId); setIt != sets.end()) { + addSearchShortcut(setIt->second.get()); } } - if (customs.empty()) { + } + if (_searchSelectedSetId + && !ranges::contains( + _searchShortcutSets, + _searchSelectedSetId, + &CustomSet::id)) { + _searchSelectedSetId = 0; + } + refreshSearchShortcutsScroll(width()); +} + +void EmojiListWidget::fillLocalSearchShortcuts(const QString &query) { + const auto searchWordsList = TextUtilities::PrepareSearchWords(query); + if (searchWordsList.isEmpty()) { + return; + } + const auto searchWordInTitle = []( + const QStringList &titleWords, + const QString &searchWord) { + for (const auto &titleWord : titleWords) { + if (titleWord.startsWith(searchWord)) { + return true; + } + } + return false; + }; + const auto allSearchWordsInTitle = [&]( + const QStringList &titleWords) { + for (const auto &searchWord : searchWordsList) { + if (!searchWordInTitle(titleWords, searchWord)) { + return false; + } + } + return true; + }; + for (const auto &set : _custom) { + if (!set.canRemove) { continue; } - const auto installed = !!(set->flags - & Data::StickersSetFlag::Installed); - _searchSets.push_back({ - .id = setId, - .set = set, - .thumbnailDocument = set->lookupThumbnailDocument(), - .title = set->title, - .list = std::move(customs), - .canRemove = installed, - }); + const auto words = TextUtilities::PrepareSearchWords( + set.title + ' ' + set.set->shortName); + if (allSearchWordsInTitle(words)) { + addSearchShortcut(set.set); + } } } +bool EmojiListWidget::addSearchShortcut(not_null set) { + if (ranges::contains(_searchShortcutSets, set->id, &CustomSet::id)) { + return false; + } + auto list = collectSearchSet(set); + if (list.empty()) { + return false; + } + const auto installed = !!(set->flags & Data::StickersSetFlag::Installed); + _searchShortcutSets.push_back({ + .id = set->id, + .set = set, + .thumbnailDocument = set->lookupThumbnailDocument(), + .title = set->title, + .list = std::move(list), + .canRemove = installed, + }); + return true; +} + +std::vector EmojiListWidget::collectSearchSet( + not_null set) { + const auto &documents = set->stickers.empty() + ? set->covers + : set->stickers; + auto result = std::vector(); + result.reserve(documents.size()); + for (const auto document : documents) { + if (const auto sticker = document->sticker()) { + const auto statusId = EmojiStatusId{ document->id }; + result.push_back({ + .custom = resolveCustomEmoji( + statusId, + document, + set->id), + .document = document, + .emoji = Ui::Emoji::Find(sticker->alt), + }); + } + } + return result; +} + +void EmojiListWidget::fillSelectedSearchShortcut() { + const auto &sets = session().data().stickers().sets(); + const auto it = sets.find(_searchSelectedSetId); + if (it == sets.end()) { + _searchSelectedSetId = 0; + return; + } + const auto set = it->second.get(); + auto list = collectSearchSet(set); + if (list.empty()) { + _searchSelectedSetId = 0; + return; + } + const auto installed = !!(set->flags & Data::StickersSetFlag::Installed); + _searchSets.push_back({ + .id = set->id, + .set = set, + .thumbnailDocument = set->lookupThumbnailDocument(), + .title = tr::lng_custom_emoji_count( + tr::now, + lt_count, + set->count), + .list = std::move(list), + .canRemove = installed, + }); +} + +bool EmojiListWidget::searchShortcutsShown() const { + return _searchMode && !_searchShortcutSets.empty(); +} + +bool EmojiListWidget::searchShortcutSelected() const { + return _searchSelectedSetId != 0; +} + +int EmojiListWidget::searchShortcutsTop() const { + return _search ? _search->height() : 0; +} + +int EmojiListWidget::searchShortcutsHeight() const { + return searchShortcutsShown() + ? ((searchShortcutSelected() ? st().searchBackHeight : 0) + + st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom) + : 0; +} + +QRect EmojiListWidget::searchBackRect() const { + return QRect( + 0, + searchShortcutsTop(), + width(), + searchShortcutSelected() ? st().searchBackHeight : 0); +} + +QRect EmojiListWidget::searchShortcutRect(int index) const { + Expects(index >= 0 && index < _searchShortcutSets.size()); + + const auto left = st().headerLeft + - st().margin.left() + - _searchShortcutsScroll + + index * (st().searchPackWidth + st().searchPackSkip); + const auto top = searchShortcutsTop() + + (searchShortcutSelected() ? st().searchBackHeight : 0) + + st().searchPacksTop; + return QRect( + left, + top, + st().searchPackWidth, + st().searchPackHeight); +} + +void EmojiListWidget::refreshSearchShortcutsScroll(int newWidth) { + if (_searchShortcutSets.empty()) { + _searchShortcutsScroll = 0; + _searchShortcutsScrollMax = 0; + return; + } + const auto count = int(_searchShortcutSets.size()); + const auto full = st().headerLeft + - st().margin.left() + + count * st().searchPackWidth + + (count - 1) * st().searchPackSkip + + st().margin.right(); + _searchShortcutsScrollMax = std::max(full - newWidth, 0); + scrollSearchShortcutsTo(_searchShortcutsScroll); +} + +void EmojiListWidget::scrollSearchShortcutsTo(int value) { + const auto scroll = std::clamp( + value, + 0, + _searchShortcutsScrollMax); + if (_searchShortcutsScroll == scroll) { + return; + } + _searchShortcutsScroll = scroll; + update(0, searchShortcutsTop(), width(), searchShortcutsHeight()); +} + +void EmojiListWidget::toggleSearchShortcut(int index) { + if (index < 0 || index >= _searchShortcutSets.size()) { + return; + } + const auto setId = _searchShortcutSets[index].id; + _searchSelectedSetId = (_searchSelectedSetId == setId) ? 0 : setId; + showSearchResults(); +} + +void EmojiListWidget::backToSearchResults() { + if (!_searchSelectedSetId) { + return; + } + _searchSelectedSetId = 0; + showSearchResults(); +} + EmojiListWidget::CustomSet &EmojiListWidget::searchSetBySection( int section) { Expects(section > 0 && section <= int(_searchSets.size())); @@ -1186,6 +1375,12 @@ void EmojiListWidget::repaintCustom(uint64 setId) { if (repaintSearch) { update(); } else { + for (auto i = 0, count = int(_searchShortcutSets.size()); + i != count; ++i) { + if (_searchShortcutSets[i].id == setId) { + rtlupdate(searchShortcutRect(i)); + } + } enumerateSections([&](const SectionInfo &info) { if (info.section > 0 && searchSetBySection(info.section).id == setId) { @@ -1389,6 +1584,7 @@ bool EmojiListWidget::enumerateSections(Callback callback) const { auto i = 0; auto info = SectionInfo(); + info.top = searchShortcutsHeight(); const auto next = [&] { info.rowsCount = info.collapsed ? kCollapsedRows @@ -1531,6 +1727,7 @@ int EmojiListWidget::countDesiredHeight(int newWidth) { + (innerWidth - _columnCount * singleWidth) / 2 - st().margin.left(); setSingleSize({ singleWidth, singleWidth - 2 * st().verticalSizeSub }); + refreshSearchShortcutsScroll(newWidth); const auto countResult = [this](int minimalLastHeight) { const auto info = sectionInfo(sectionsCount() - 1); @@ -1813,11 +2010,130 @@ void EmojiListWidget::validateEmojiPaintContext( } } +void EmojiListWidget::paintSearchShortcuts(QPainter &p, QRect clip) { + if (!searchShortcutsShown() + || clip.bottom() < searchShortcutsTop() + || clip.top() >= searchShortcutsTop() + searchShortcutsHeight()) { + return; + } + if (searchShortcutSelected()) { + const auto back = searchBackRect(); + const auto selected = std::get_if( + !v::is_null(_pressed) ? &_pressed : &_selected); + const auto &icon = selected + ? st().search.back.iconOver + : st().search.back.icon; + icon.paint( + p, + st().searchBackIconLeft, + back.y() + st().searchBackIconTop, + width()); + const auto text = tr::lng_search_back_to_results(tr::now); + const auto &font = st::emojiPanHeaderFont; + const auto available = width() + - st().searchBackTextLeft + - st().margin.right(); + auto shown = text; + auto textWidth = font->width(shown); + if (textWidth > available) { + shown = font->elided(shown, available); + textWidth = font->width(shown); + } + p.setFont(font); + p.setPen(st().headerFg); + p.drawText( + st().searchBackTextLeft, + back.y() + st().searchBackTextTop + font->ascent, + shown); + } + + const auto selectedShortcut = std::get_if( + !v::is_null(_pressed) ? &_pressed : &_selected); + p.save(); + p.setClipRect( + QRect( + 0, + searchShortcutsTop() + + (searchShortcutSelected() ? st().searchBackHeight : 0), + width(), + st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom), + Qt::IntersectClip); + for (auto i = 0, count = int(_searchShortcutSets.size()); i != count; ++i) { + auto &set = _searchShortcutSets[i]; + const auto rect = searchShortcutRect(i); + if (!rect.intersects(clip)) { + continue; + } + const auto selected = (set.id == _searchSelectedSetId) + || (selectedShortcut && selectedShortcut->index == i); + if (selected) { + _overBg.paint(p, myrtlrect(rect)); + } + if (set.ripple) { + set.ripple->paint( + p, + myrtlrect(rect).x(), + rect.y(), + width()); + if (set.ripple->empty()) { + set.ripple.reset(); + } + } + const auto icon = QRect( + rect.x() + (rect.width() - st().searchPackIconSize) / 2, + rect.y() + st().searchPackIconTop, + st().searchPackIconSize, + st().searchPackIconSize); + paintSearchShortcutIcon(p, set, icon); + + const auto available = rect.width() + - 2 * st().searchPackTextPadding; + auto title = set.title; + auto titleWidth = st::normalFont->width(title); + if (titleWidth > available) { + title = st::normalFont->elided(title, available); + titleWidth = st::normalFont->width(title); + } + p.setFont(st::normalFont); + p.setPen(st().textFg); + p.drawText( + rect.x() + st().searchPackTextPadding, + rect.y() + st().searchPackTextTop + st::normalFont->ascent, + title); + } + p.restore(); +} + +void EmojiListWidget::paintSearchShortcutIcon( + QPainter &p, + const CustomSet &set, + QRect rect) { + if (set.list.empty()) { + return; + } + auto context = Ui::Text::CustomEmojiPaintContext{ + .textColor = (_customTextColor + ? _customTextColor() + : st().textFg->c), + .size = rect.size(), + .now = crl::now(), + .scale = 1., + .position = rect.topLeft(), + .paused = On(powerSavingFlag()) || paused(), + .scaled = false, + .internal = { .forceFirstFrame = (_mode == Mode::BackgroundEmoji) }, + }; + set.list.front().custom->paint(p, context); +} + void EmojiListWidget::paint( Painter &p, ExpandingContext context, QRect clip) { validateEmojiPaintContext(context); + paintSearchShortcuts(p, clip); _paintAsPremium = session().premium(); @@ -1842,6 +2158,7 @@ void EmojiListWidget::paint( : &_selected); if (_searchResults.empty() && _searchSets.empty() + && _searchShortcutSets.empty() && _searchMode && !_searchLoading && !_searchRequestTimer.isActive()) { @@ -2287,6 +2604,11 @@ void EmojiListWidget::mousePressEvent(QMouseEvent *e) { return; } setPressed(_selected); + if (std::get_if(&_selected)) { + _searchShortcutsMouseDown = _lastMousePos; + _searchShortcutsDragStart = _searchShortcutsScroll; + _searchShortcutsDragging = false; + } if (const auto over = std::get_if(&_selected)) { const auto emoji = lookupOverEmoji(over); if (emoji && emoji->hasVariants()) { @@ -2325,6 +2647,10 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { } } updateSelected(); + if (_searchShortcutsDragging) { + _searchShortcutsDragging = false; + return; + } if (_showPickerTimer.isActive()) { _showPickerTimer.cancel(); @@ -2342,7 +2668,14 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { return; } - if (const auto over = std::get_if(&_selected)) { + if (std::get_if(&_selected)) { + backToSearchResults(); + return; + } else if (const auto shortcut = std::get_if( + &_selected)) { + toggleSearchShortcut(shortcut->index); + return; + } else if (const auto over = std::get_if(&_selected)) { const auto section = over->section; const auto index = over->index; if (sectionInfo(section).collapsed @@ -2749,6 +3082,20 @@ void EmojiListWidget::colorChosen(EmojiChosen data) { void EmojiListWidget::mouseMoveEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); + if (std::get_if(&_pressed) + && _searchShortcutsScrollMax > 0) { + const auto delta = _lastMousePos - _searchShortcutsMouseDown; + if (!_searchShortcutsDragging + && delta.manhattanLength() >= QApplication::startDragDistance()) { + _searchShortcutsDragging = true; + } + if (_searchShortcutsDragging) { + scrollSearchShortcutsTo( + _searchShortcutsDragStart + + (rtl() ? -1 : 1) * -delta.x()); + return; + } + } if (!_picker->isHidden()) { if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) { return _picker->handleMouseMove(QCursor::pos()); @@ -3303,6 +3650,23 @@ void EmojiListWidget::updateSelected() { auto newSelected = OverState{ v::null }; auto p = mapFromGlobal(_lastMousePos); + if (searchShortcutsShown() + && p.y() >= searchShortcutsTop() + && p.y() < searchShortcutsTop() + searchShortcutsHeight()) { + if (searchShortcutSelected() && searchBackRect().contains(p)) { + newSelected = OverSearchBack{}; + } else { + for (auto i = 0, count = int(_searchShortcutSets.size()); + i != count; ++i) { + if (myrtlrect(searchShortcutRect(i)).contains(p)) { + newSelected = OverSearchShortcut{ i }; + break; + } + } + } + setSelected(newSelected); + return; + } auto info = sectionInfoByOffset(p.y()); auto section = info.section; if (p.y() >= info.top && p.y() < info.rowsTop) { @@ -3339,6 +3703,14 @@ void EmojiListWidget::setSelected(OverState newSelected) { rtlupdate(emojiRect(sticker->section, sticker->index)); } else if (const auto button = std::get_if(&_selected)) { rtlupdate(buttonRect(button->section)); + } else if (const auto shortcut + = std::get_if(&_selected)) { + if (shortcut->index >= 0 + && shortcut->index < _searchShortcutSets.size()) { + rtlupdate(searchShortcutRect(shortcut->index)); + } + } else if (std::get_if(&_selected)) { + rtlupdate(searchBackRect()); } }; updateSelected(); @@ -3382,6 +3754,14 @@ void EmojiListWidget::setPressed(OverState newPressed) { if (ripple) { ripple->lastStop(); } + } else if (auto shortcut = std::get_if(&_pressed)) { + if (shortcut->index >= 0 + && shortcut->index < _searchShortcutSets.size()) { + auto &ripple = _searchShortcutSets[shortcut->index].ripple; + if (ripple) { + ripple->lastStop(); + } + } } _pressed = newPressed; if (auto button = std::get_if(&_pressed)) { @@ -3399,6 +3779,16 @@ void EmojiListWidget::setPressed(OverState newPressed) { ripple = createButtonRipple(button->section); } ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section)); + } else if (auto shortcut = std::get_if(&_pressed)) { + if (shortcut->index >= 0 + && shortcut->index < _searchShortcutSets.size()) { + auto &ripple = _searchShortcutSets[shortcut->index].ripple; + if (!ripple) { + ripple = createSearchShortcutRipple(shortcut->index); + } + ripple->add(mapFromGlobal(QCursor::pos()) + - myrtlrect(searchShortcutRect(shortcut->index)).topLeft()); + } } } @@ -3472,6 +3862,29 @@ QPoint EmojiListWidget::buttonRippleTopLeft(int section) const { : QPoint()); } +std::unique_ptr +EmojiListWidget::createSearchShortcutRipple(int index) { + Expects(index >= 0 && index < _searchShortcutSets.size()); + + const auto setId = _searchShortcutSets[index].id; + auto mask = Ui::RippleAnimation::RoundRectMask( + searchShortcutRect(index).size(), + st::roundRadiusLarge); + return std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [this, setId] { + const auto i = ranges::find( + _searchShortcutSets, + setId, + &CustomSet::id); + if (i != _searchShortcutSets.end()) { + rtlupdate(searchShortcutRect( + int(i - _searchShortcutSets.begin()))); + } + }); +} + PowerSaving::Flag EmojiListWidget::powerSavingFlag() const { const auto reactions = (_mode == Mode::FullReactions) || (_mode == Mode::RecentReactions); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index afa38c9fbe..8e55d83c9d 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -252,11 +252,31 @@ private: return !(*this == other); } }; + struct OverSearchShortcut { + int index = 0; + + inline bool operator==(OverSearchShortcut other) const { + return (index == other.index); + } + inline bool operator!=(OverSearchShortcut other) const { + return !(*this == other); + } + }; + struct OverSearchBack { + inline bool operator==(OverSearchBack other) const { + return true; + } + inline bool operator!=(OverSearchBack other) const { + return !(*this == other); + } + }; using OverState = std::variant< v::null_t, OverEmoji, OverSet, - OverButton>; + OverButton, + OverSearchShortcut, + OverSearchBack>; struct ExpandingContext { float64 progress = 0.; int finalHeight = 0; @@ -318,7 +338,24 @@ private: const MTPmessages_FoundStickerSets &result); void showSearchResults(); void fillCloudSearchResults(); - void fillCloudSearchSets(); + void refreshSearchShortcuts(); + void fillLocalSearchShortcuts(const QString &query); + bool addSearchShortcut(not_null set); + [[nodiscard]] std::vector collectSearchSet( + not_null set); + void fillSelectedSearchShortcut(); + [[nodiscard]] bool searchShortcutsShown() const; + [[nodiscard]] bool searchShortcutSelected() const; + [[nodiscard]] int searchShortcutsHeight() const; + [[nodiscard]] int searchShortcutsTop() const; + [[nodiscard]] QRect searchBackRect() const; + [[nodiscard]] QRect searchShortcutRect(int index) const; + void refreshSearchShortcutsScroll(int newWidth); + void scrollSearchShortcutsTo(int value); + void paintSearchShortcuts(QPainter &p, QRect clip); + void paintSearchShortcutIcon(QPainter &p, const CustomSet &set, QRect rect); + void toggleSearchShortcut(int index); + void backToSearchResults(); [[nodiscard]] CustomSet &searchSetBySection(int section); [[nodiscard]] const CustomSet &searchSetBySection(int section) const; void ensureLoaded(int section); @@ -411,6 +448,8 @@ private: [[nodiscard]] std::unique_ptr createButtonRipple( int section); [[nodiscard]] QPoint buttonRippleTopLeft(int section) const; + [[nodiscard]] std::unique_ptr + createSearchShortcutRipple(int index); [[nodiscard]] PowerSaving::Flag powerSavingFlag() const; void repaintCustom(uint64 setId); @@ -497,9 +536,16 @@ private: std::map _searchCloudNextOffset; std::map> _searchSetsCache; std::vector _searchSets; + std::vector _searchShortcutSets; QString _searchRequestQuery; QString _searchNextRequestQuery; QString _searchEmoticon; + uint64 _searchSelectedSetId = 0; + int _searchShortcutsScroll = 0; + int _searchShortcutsScrollMax = 0; + int _searchShortcutsDragStart = 0; + QPoint _searchShortcutsMouseDown; + bool _searchShortcutsDragging = false; mtpRequestId _searchCloudRequestId = 0; mtpRequestId _searchSetsRequestId = 0; bool _searchLoading = false; From c23f082d07e1a0d75cba0ba0d06fa95d06601f34 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:07:41 +0300 Subject: [PATCH 016/154] Collapsed pack shortcut strip when sticker/emoji pack is selected. --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 6 +++--- Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 19fe615572..caf0634d8e 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1284,7 +1284,7 @@ QRect EmojiListWidget::searchBackRect() const { } QRect EmojiListWidget::searchShortcutRect(int index) const { - Expects(index >= 0 && index < _searchShortcutSets.size()); + Expects(index >= 0 && index < int(_searchShortcutSets.size())); const auto left = st().headerLeft - st().margin.left() @@ -1310,7 +1310,7 @@ void EmojiListWidget::refreshSearchShortcutsScroll(int newWidth) { const auto full = st().headerLeft - st().margin.left() + count * st().searchPackWidth - + (count - 1) * st().searchPackSkip + + std::max(count - 1, 0) * st().searchPackSkip + st().margin.right(); _searchShortcutsScrollMax = std::max(full - newWidth, 0); scrollSearchShortcutsTo(_searchShortcutsScroll); @@ -1329,7 +1329,7 @@ void EmojiListWidget::scrollSearchShortcutsTo(int value) { } void EmojiListWidget::toggleSearchShortcut(int index) { - if (index < 0 || index >= _searchShortcutSets.size()) { + if (index < 0 || index >= int(_searchShortcutSets.size())) { return; } const auto setId = _searchShortcutSets[index].id; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 16ccd46ade..29b49d3e8b 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -946,7 +946,7 @@ QRect StickersListWidget::searchBackRect() const { } QRect StickersListWidget::searchShortcutRect(int index) const { - Expects(index >= 0 && index < _searchShortcutSets.size()); + Expects(index >= 0 && index < int(_searchShortcutSets.size())); const auto left = st().headerLeft - st().margin.left() @@ -972,7 +972,7 @@ void StickersListWidget::refreshSearchShortcutsScroll(int newWidth) { const auto full = st().headerLeft - st().margin.left() + count * st().searchPackWidth - + (count - 1) * st().searchPackSkip + + std::max(count - 1, 0) * st().searchPackSkip + st().margin.right(); _searchShortcutsScrollMax = std::max(full - newWidth, 0); scrollSearchShortcutsTo(_searchShortcutsScroll); @@ -991,7 +991,7 @@ void StickersListWidget::scrollSearchShortcutsTo(int value) { } void StickersListWidget::toggleSearchShortcut(int index) { - if (index < 0 || index >= _searchShortcutSets.size()) { + if (index < 0 || index >= int(_searchShortcutSets.size())) { return; } const auto setId = _searchShortcutSets[index].id; From 2f86d2a04c6e0ec99de5660344c376773ec26ba9 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:08:15 +0300 Subject: [PATCH 017/154] Fixed empty results when selected search pack disappears. --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 3 ++- Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index caf0634d8e..eb1aea49f8 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1080,7 +1080,8 @@ void EmojiListWidget::showSearchResults() { refreshSearchShortcuts(); if (searchShortcutSelected()) { fillSelectedSearchShortcut(); - } else { + } + if (!searchShortcutSelected()) { auto plain = collectPlainSearchResults(); if (_mode == Mode::Full) { for (const auto emoji : plain) { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 29b49d3e8b..ba2f5b0139 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -781,7 +781,8 @@ void StickersListWidget::refreshSearchRows( } if (searchShortcutSelected()) { fillSelectedSearchShortcut(); - } else { + } + if (!searchShortcutSelected()) { fillFilteredStickersRow(); if (hasCloudFoundStickers) { fillFoundStickersRow(foundStickersIt->second); From dc0fc34ad1970fedc47f26326b2dd86ca8e7847c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:08:43 +0300 Subject: [PATCH 018/154] Forced static first frame for emoji search pack icons. --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index eb1aea49f8..e701eacf57 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -2124,7 +2124,7 @@ void EmojiListWidget::paintSearchShortcutIcon( .position = rect.topLeft(), .paused = On(powerSavingFlag()) || paused(), .scaled = false, - .internal = { .forceFirstFrame = (_mode == Mode::BackgroundEmoji) }, + .internal = { .forceFirstFrame = true }, }; set.list.front().custom->paint(p, context); } From dc73680d0c4312dde757f7a136a44fd3d5fd00c3 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:09:35 +0300 Subject: [PATCH 019/154] Respected RTL text painting in emoji search pack strip. --- .../chat_helpers/emoji_list_widget.cpp | 20 +++++++++++-------- .../chat_helpers/emoji_list_widget.h | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index e701eacf57..134c4c5889 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -2011,7 +2011,7 @@ void EmojiListWidget::validateEmojiPaintContext( } } -void EmojiListWidget::paintSearchShortcuts(QPainter &p, QRect clip) { +void EmojiListWidget::paintSearchShortcuts(Painter &p, QRect clip) { if (!searchShortcutsShown() || clip.bottom() < searchShortcutsTop() || clip.top() >= searchShortcutsTop() + searchShortcutsHeight()) { @@ -2042,10 +2042,12 @@ void EmojiListWidget::paintSearchShortcuts(QPainter &p, QRect clip) { } p.setFont(font); p.setPen(st().headerFg); - p.drawText( + p.drawTextLeft( st().searchBackTextLeft, - back.y() + st().searchBackTextTop + font->ascent, - shown); + back.y() + st().searchBackTextTop, + width(), + shown, + textWidth); } const auto selectedShortcut = std::get_if( @@ -2099,16 +2101,18 @@ void EmojiListWidget::paintSearchShortcuts(QPainter &p, QRect clip) { } p.setFont(st::normalFont); p.setPen(st().textFg); - p.drawText( + p.drawTextLeft( rect.x() + st().searchPackTextPadding, - rect.y() + st().searchPackTextTop + st::normalFont->ascent, - title); + rect.y() + st().searchPackTextTop, + width(), + title, + titleWidth); } p.restore(); } void EmojiListWidget::paintSearchShortcutIcon( - QPainter &p, + Painter &p, const CustomSet &set, QRect rect) { if (set.list.empty()) { diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 8e55d83c9d..586518ce05 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -352,8 +352,8 @@ private: [[nodiscard]] QRect searchShortcutRect(int index) const; void refreshSearchShortcutsScroll(int newWidth); void scrollSearchShortcutsTo(int value); - void paintSearchShortcuts(QPainter &p, QRect clip); - void paintSearchShortcutIcon(QPainter &p, const CustomSet &set, QRect rect); + void paintSearchShortcuts(Painter &p, QRect clip); + void paintSearchShortcutIcon(Painter &p, const CustomSet &set, QRect rect); void toggleSearchShortcut(int index); void backToSearchResults(); [[nodiscard]] CustomSet &searchSetBySection(int section); From fa8fdaecdd0092f4a84970152874de7672a43d09 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:11:14 +0300 Subject: [PATCH 020/154] Limited emoji shortcut payload to one icon per pack. --- .../chat_helpers/emoji_list_widget.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 134c4c5889..1499a95cc4 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1189,7 +1189,23 @@ bool EmojiListWidget::addSearchShortcut(not_null set) { if (ranges::contains(_searchShortcutSets, set->id, &CustomSet::id)) { return false; } - auto list = collectSearchSet(set); + const auto &documents = set->stickers.empty() + ? set->covers + : set->stickers; + auto list = std::vector(); + for (const auto document : documents) { + if (const auto sticker = document->sticker()) { + list.push_back({ + .custom = resolveCustomEmoji( + EmojiStatusId{ document->id }, + document, + set->id), + .document = document, + .emoji = Ui::Emoji::Find(sticker->alt), + }); + break; + } + } if (list.empty()) { return false; } From 016fc0e15c6f1c0b7fcb3fe1c74d3b1b0db3bbb7 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:13:30 +0300 Subject: [PATCH 021/154] Factored search-words matcher used by pack shortcuts. --- .../chat_helpers/emoji_list_widget.cpp | 21 +----------------- .../chat_helpers/stickers_list_footer.cpp | 18 +++++++++++++++ .../chat_helpers/stickers_list_footer.h | 4 ++++ .../chat_helpers/stickers_list_widget.cpp | 22 +------------------ 4 files changed, 24 insertions(+), 41 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 1499a95cc4..c0367b4f37 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1154,32 +1154,13 @@ void EmojiListWidget::fillLocalSearchShortcuts(const QString &query) { if (searchWordsList.isEmpty()) { return; } - const auto searchWordInTitle = []( - const QStringList &titleWords, - const QString &searchWord) { - for (const auto &titleWord : titleWords) { - if (titleWord.startsWith(searchWord)) { - return true; - } - } - return false; - }; - const auto allSearchWordsInTitle = [&]( - const QStringList &titleWords) { - for (const auto &searchWord : searchWordsList) { - if (!searchWordInTitle(titleWords, searchWord)) { - return false; - } - } - return true; - }; for (const auto &set : _custom) { if (!set.canRemove) { continue; } const auto words = TextUtilities::PrepareSearchWords( set.title + ' ' + set.set->shortName); - if (allSearchWordsInTitle(words)) { + if (MatchAllPreparedSearchWords(words, searchWordsList)) { addSearchShortcut(set.set); } } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index 8971ceec58..004075748c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -63,6 +63,24 @@ void UpdateAnimated( } // namespace +bool MatchAllPreparedSearchWords( + const QStringList &titleWords, + const QStringList &searchWords) { + for (const auto &searchWord : searchWords) { + auto found = false; + for (const auto &titleWord : titleWords) { + if (titleWord.startsWith(searchWord)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; +} + uint64 EmojiSectionSetId(EmojiSection section) { Expects(section >= EmojiSection::Recent && section <= EmojiSection::Symbols); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h index e5910d60c4..2da22cb57c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h @@ -116,6 +116,10 @@ private: }; +[[nodiscard]] bool MatchAllPreparedSearchWords( + const QStringList &titleWords, + const QStringList &searchWords); + class StickersListFooter final : public TabbedSelector::InnerFooter { public: struct Descriptor { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index ba2f5b0139..1fe95bf8a8 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -835,29 +835,9 @@ void StickersListWidget::fillLocalSearchShortcuts(const QString &query) { if (searchWordsList.isEmpty()) { return; } - const auto searchWordInTitle = []( - const QStringList &titleWords, - const QString &searchWord) { - for (const auto &titleWord : titleWords) { - if (titleWord.startsWith(searchWord)) { - return true; - } - } - return false; - }; - const auto allSearchWordsInTitle = [&]( - const QStringList &titleWords) { - for (const auto &searchWord : searchWordsList) { - if (!searchWordInTitle(titleWords, searchWord)) { - return false; - } - } - return true; - }; - const auto &sets = session().data().stickers().sets(); for (const auto &[setId, titleWords] : _searchIndex) { - if (!allSearchWordsInTitle(titleWords)) { + if (!MatchAllPreparedSearchWords(titleWords, searchWordsList)) { continue; } else if (const auto it = sets.find(setId); it != sets.end()) { addSearchShortcut(it->second.get()); From bf98379b91e7f3872ef57bdd6093779219e1a288 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:14:35 +0300 Subject: [PATCH 022/154] Split shortcut refresh from search-set refresh in stickers. --- .../chat_helpers/stickers_list_widget.cpp | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 1fe95bf8a8..8781fcfe7c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -2857,34 +2857,45 @@ void StickersListWidget::refreshSearchSets() { const auto &sets = session().data().stickers().sets(); const auto skipPremium = !session().premiumPossible(); - const auto refresh = [&](Set &entry) { - if (const auto it = sets.find(entry.id); it != sets.end()) { - const auto set = it->second.get(); - const auto selected = (_searchSelectedSetId == entry.id); - entry.flags = selected - ? (set->flags | SetFlag::Special) - : set->flags; - auto elements = PrepareStickers( - set->stickers.empty() ? set->covers : set->stickers, - skipPremium); - if (!elements.empty()) { - entry.lottiePlayer = nullptr; - entry.stickers = std::move(elements); - } - entry.thumbnailDocument = set->lookupThumbnailDocument(); - if (selected) { - entry.externalLayout = false; - } else if (!SetInMyList(entry.flags)) { - _localSetsManager->removeInstalledLocally(entry.id); - entry.externalLayout = true; - } + const auto refreshElements = [&](Set &entry, not_null set) { + auto elements = PrepareStickers( + set->stickers.empty() ? set->covers : set->stickers, + skipPremium); + if (!elements.empty()) { + entry.lottiePlayer = nullptr; + entry.stickers = std::move(elements); } + entry.thumbnailDocument = set->lookupThumbnailDocument(); }; for (auto &entry : _searchSets) { - refresh(entry); + const auto it = sets.find(entry.id); + if (it == sets.end()) { + continue; + } + const auto set = it->second.get(); + const auto selected = (_searchSelectedSetId == entry.id); + entry.flags = selected + ? (set->flags | SetFlag::Special) + : set->flags; + refreshElements(entry, set); + entry.title = selected + ? tr::lng_stickers_count(tr::now, lt_count, set->count) + : set->title; + if (selected) { + entry.externalLayout = false; + } else if (!SetInMyList(entry.flags)) { + _localSetsManager->removeInstalledLocally(entry.id); + entry.externalLayout = true; + } } for (auto &entry : _searchShortcutSets) { - refresh(entry); + const auto it = sets.find(entry.id); + if (it == sets.end()) { + continue; + } + const auto set = it->second.get(); + entry.title = set->title; + refreshElements(entry, set); } } From 523e2185eef6528f5ebe2e193d7e25450aa4a8ed Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:29:59 +0300 Subject: [PATCH 023/154] Added label under shortcut strip in emoji/stickers list widgets. --- Telegram/Resources/langs/lang.strings | 1 + .../chat_helpers/emoji_list_widget.cpp | 32 +++++++++++++++---- .../chat_helpers/stickers_list_widget.cpp | 32 +++++++++++++++---- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 51d64ca353..6cca5b3ede 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4483,6 +4483,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_custom_emoji_count#one" = "{count} emoji"; "lng_custom_emoji_count#other" = "{count} emoji"; "lng_search_back_to_results" = "Back to search"; +"lng_search_results_header" = "Search Result"; "lng_stickers_attached_sets" = "Sets of attached stickers"; "lng_custom_emoji_used_sets" = "Sets of used emoji"; "lng_custom_emoji_remove_pack_button" = "Remove Emoji"; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index c0367b4f37..a738c8368b 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1265,12 +1265,18 @@ int EmojiListWidget::searchShortcutsTop() const { } int EmojiListWidget::searchShortcutsHeight() const { - return searchShortcutsShown() - ? ((searchShortcutSelected() ? st().searchBackHeight : 0) - + st().searchPacksTop - + st().searchPackHeight - + st().searchPacksBottom) - : 0; + if (!searchShortcutsShown()) { + return 0; + } + auto result = st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom; + if (searchShortcutSelected()) { + result += st().searchBackHeight; + } else { + result += st().header; + } + return result; } QRect EmojiListWidget::searchBackRect() const { @@ -2106,6 +2112,20 @@ void EmojiListWidget::paintSearchShortcuts(Painter &p, QRect clip) { titleWidth); } p.restore(); + + if (!searchShortcutSelected()) { + const auto top = searchShortcutsTop() + + st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom; + p.setFont(st::emojiPanHeaderFont); + p.setPen(st().headerFg); + p.drawTextLeft( + st().headerLeft - st().margin.left(), + top + st().headerTop, + width(), + tr::lng_search_results_header(tr::now)); + } } void EmojiListWidget::paintSearchShortcutIcon( diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 8781fcfe7c..52f4a7b027 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -910,12 +910,18 @@ int StickersListWidget::searchShortcutsTop() const { } int StickersListWidget::searchShortcutsHeight() const { - return searchShortcutsShown() - ? ((searchShortcutSelected() ? st().searchBackHeight : 0) - + st().searchPacksTop - + st().searchPackHeight - + st().searchPacksBottom) - : 0; + if (!searchShortcutsShown()) { + return 0; + } + auto result = st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom; + if (searchShortcutSelected()) { + result += st().searchBackHeight; + } else { + result += st().header; + } + return result; } QRect StickersListWidget::searchBackRect() const { @@ -1389,6 +1395,20 @@ void StickersListWidget::paintSearchShortcuts(Painter &p, QRect clip) { titleWidth); } p.restore(); + + if (!searchShortcutSelected()) { + const auto top = searchShortcutsTop() + + st().searchPacksTop + + st().searchPackHeight + + st().searchPacksBottom; + p.setFont(st::emojiPanHeaderFont); + p.setPen(st().headerFg); + p.drawTextLeft( + st().headerLeft - st().margin.left(), + top + st().headerTop, + width(), + tr::lng_search_results_header(tr::now)); + } } void StickersListWidget::paintSearchShortcutIcon( From 31e94599957906bc3daadbcb8194b3c4c720f58f Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 15:38:33 +0300 Subject: [PATCH 024/154] Slightly improved search-result band and back-to-search row paddings. --- Telegram/SourceFiles/chat_helpers/chat_helpers.style | 10 +++++++--- .../SourceFiles/chat_helpers/emoji_list_widget.cpp | 4 ++-- .../SourceFiles/chat_helpers/stickers_list_widget.cpp | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index fb280209d9..84844e7686 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -131,6 +131,8 @@ EmojiPan { searchPackTextTop: pixels; searchPackTextPadding: pixels; searchPacksBottom: pixels; + searchResultsHeight: pixels; + searchResultsTextTop: pixels; searchBackHeight: pixels; searchBackIconLeft: pixels; searchBackIconTop: pixels; @@ -783,7 +785,7 @@ defaultEmojiPan: EmojiPan { tabs: emojiTabs; search: defaultTabbedSearch; searchMargin: margins(1px, 11px, 2px, 5px); - searchPacksTop: 4px; + searchPacksTop: 0px; searchPackWidth: 70px; searchPackHeight: 78px; searchPackSkip: 6px; @@ -791,8 +793,10 @@ defaultEmojiPan: EmojiPan { searchPackIconTop: 8px; searchPackTextTop: 55px; searchPackTextPadding: 5px; - searchPacksBottom: 4px; - searchBackHeight: 40px; + searchPacksBottom: 0px; + searchResultsHeight: 20px; + searchResultsTextTop: 3px; + searchBackHeight: 36px; searchBackIconLeft: 5px; searchBackIconTop: 6px; searchBackTextLeft: 40px; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index a738c8368b..69d685a5a4 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1274,7 +1274,7 @@ int EmojiListWidget::searchShortcutsHeight() const { if (searchShortcutSelected()) { result += st().searchBackHeight; } else { - result += st().header; + result += st().searchResultsHeight; } return result; } @@ -2122,7 +2122,7 @@ void EmojiListWidget::paintSearchShortcuts(Painter &p, QRect clip) { p.setPen(st().headerFg); p.drawTextLeft( st().headerLeft - st().margin.left(), - top + st().headerTop, + top + st().searchResultsTextTop, width(), tr::lng_search_results_header(tr::now)); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 52f4a7b027..c366ffe58f 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -919,7 +919,7 @@ int StickersListWidget::searchShortcutsHeight() const { if (searchShortcutSelected()) { result += st().searchBackHeight; } else { - result += st().header; + result += st().searchResultsHeight; } return result; } @@ -1405,7 +1405,7 @@ void StickersListWidget::paintSearchShortcuts(Painter &p, QRect clip) { p.setPen(st().headerFg); p.drawTextLeft( st().headerLeft - st().margin.left(), - top + st().headerTop, + top + st().searchResultsTextTop, width(), tr::lng_search_results_header(tr::now)); } From 6791fb851f8dabcc4e052ae2647a3ecdcb3c8a02 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 12:52:36 +0300 Subject: [PATCH 025/154] Dropped top padding of first sticker section under shortcut strip. --- Telegram/SourceFiles/chat_helpers/chat_helpers.style | 1 + Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 8 +++++++- .../SourceFiles/chat_helpers/stickers_list_widget.cpp | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 84844e7686..1b45f2ea99 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -622,6 +622,7 @@ stickerPanRemoveSet: IconButton(hashtagClose) { stickerIconMove: 400; stickerPreviewDuration: 150; stickerPreviewMin: 0.1; +stickerPanFirstAfterShortcutsSkip: 4px; mediaPreviewPhotoSkip: 48px; emojiPanColorAll: IconButton(stickerPanRemoveSet) { diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 69d685a5a4..b3b0e354b2 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1593,8 +1593,14 @@ bool EmojiListWidget::enumerateSections(Callback callback) const { info.rowsCount = info.collapsed ? kCollapsedRows : (info.count + _columnCount - 1) / _columnCount; + const auto firstAfterShortcuts = !i + && searchShortcutsShown() + && !searchShortcutSelected(); info.rowsTop = info.top - + (i == 0 ? _rowsTop : st().header); + + (i == 0 ? _rowsTop : st().header) + + (firstAfterShortcuts + ? st::stickerPanFirstAfterShortcutsSkip + : 0); info.rowsBottom = info.rowsTop + (info.rowsCount * _singleSize.height()); if (!callback(info)) { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index c366ffe58f..1f59a650ee 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -504,10 +504,15 @@ bool StickersListWidget::enumerateSections(Callback callback) const { auto &set = sets[i]; info.section = i; info.count = set.stickers.size(); + const auto firstAfterShortcuts = !i + && searchShortcutsShown() + && !searchShortcutSelected(); const auto titleSkip = set.externalLayout ? st::stickersTrendingHeader : setHasTitle(set) ? st().header + : firstAfterShortcuts + ? st::stickerPanFirstAfterShortcutsSkip : st::stickerPanPadding; info.rowsTop = info.top + titleSkip; if (set.externalLayout) { From 7c0b1f43fad35bc4238afa2ffa556da214363551 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 13:04:40 +0300 Subject: [PATCH 026/154] Dispatched sticker pack search in parallel with sticker search. --- .../chat_helpers/stickers_list_widget.cpp | 78 +++++++++---------- .../chat_helpers/stickers_list_widget.h | 4 +- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 1f59a650ee..47593dae6f 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -609,32 +609,35 @@ int StickersListWidget::countDesiredHeight(int newWidth) { } void StickersListWidget::sendSearchRequest() { - if (_searchSetsRequestId - || _searchStickersRequestId - || _searchNextQuery.isEmpty() - || _isEffects) { + if (_searchNextQuery.isEmpty() || _isEffects) { return; } - _searchRequestTimer.cancel(); _searchQuery = _searchNextQuery; - auto it = _searchStickersCache.find(_searchQuery); - if (it != _searchStickersCache.cend()) { - toggleSearchLoading(false); - return; - } - toggleSearchLoading(true); if (_searchQuery == Ui::PremiumGroupFakeEmoticon()) { toggleSearchLoading(false); - _searchSetsRequestId = 0; _searchSetsCache.emplace(_searchQuery, std::vector()); _searchStickersCache.emplace(_searchQuery, std::vector()); showSearchResults(); return; } - requestSearchStickers(_searchQuery, 0, true); + const auto stickersCached = (_searchStickersCache.find(_searchQuery) + != _searchStickersCache.cend()); + const auto setsCached = (_searchSetsCache.find(_searchQuery) + != _searchSetsCache.cend()); + if (stickersCached && setsCached) { + toggleSearchLoading(false); + return; + } + toggleSearchLoading(true); + if (!stickersCached && !_searchStickersRequestId) { + requestSearchStickers(_searchQuery, 0, true); + } + if (!setsCached && !_searchSetsRequestId) { + sendSearchSetsRequest(_searchQuery); + } } void StickersListWidget::sendSearchSetsRequest(const QString &query) { @@ -647,7 +650,8 @@ void StickersListWidget::sendSearchSetsRequest(const QString &query) { searchResultsDone(query, result); }).fail([=] { _searchSetsRequestId = 0; - if (_searchNextQuery == query) { + _searchSetsCache.emplace(query, std::vector()); + if (_searchNextQuery == query && !_searchStickersRequestId) { toggleSearchLoading(false); } }).handleAllErrors().send(); @@ -656,7 +660,7 @@ void StickersListWidget::sendSearchSetsRequest(const QString &query) { void StickersListWidget::requestSearchStickers( const QString &query, int offset, - bool requestSetsOnEmpty) { + bool isInitial) { const auto hash = uint64(0); _searchStickersRequestId = _api.request(MTPmessages_SearchStickers( MTP_flags(0), @@ -667,18 +671,12 @@ void StickersListWidget::requestSearchStickers( MTP_int(50), MTP_long(hash) )).done([=](const MTPmessages_FoundStickers &result) { - searchStickersResultsDone( - query, - offset, - requestSetsOnEmpty, - result); + searchStickersResultsDone(query, offset, isInitial, result); }).fail([=] { _searchStickersRequestId = 0; - if (requestSetsOnEmpty) { - _searchStickersCache.emplace(query, std::vector()); - if (_searchNextQuery == query) { - sendSearchSetsRequest(query); - } + _searchStickersCache.emplace(query, std::vector()); + if (_searchNextQuery == query && !_searchSetsRequestId) { + toggleSearchLoading(false); } }).handleAllErrors().send(); } @@ -712,7 +710,7 @@ void StickersListWidget::searchForSets( _api.request(requestId).cancel(); } if (_searchStickersCache.find(cleaned) != _searchStickersCache.cend() - || _searchSetsCache.find(cleaned) != _searchSetsCache.cend()) { + && _searchSetsCache.find(cleaned) != _searchSetsCache.cend()) { _searchRequestTimer.cancel(); _searchQuery = _searchNextQuery = cleaned; } else { @@ -1139,10 +1137,15 @@ auto StickersListWidget::shownSets() -> std::vector & { void StickersListWidget::searchStickersResultsDone( const QString &query, int requestedOffset, - bool requestSetsOnEmpty, + bool isInitial, const MTPmessages_FoundStickers &result) { _searchStickersRequestId = 0; const auto active = (_searchNextQuery == query); + const auto finishLoading = [&] { + if (active && !_searchSetsRequestId) { + toggleSearchLoading(false); + } + }; result.match([&](const MTPDmessages_foundStickersNotModified &data) { LOG(("API: messages.foundStickersNotModified.")); @@ -1159,11 +1162,12 @@ void StickersListWidget::searchStickersResultsDone( if (!active) { return; } - if (requestSetsOnEmpty) { - sendSearchSetsRequest(query); - return; + finishLoading(); + if (isInitial) { + showSearchResults(); + } else { + refreshSearchRows(); } - refreshSearchRows(); checkPaginateSearchStickers( getVisibleTop(), getVisibleBottom()); @@ -1194,12 +1198,8 @@ void StickersListWidget::searchStickersResultsDone( if (!active) { return; } - if (requestSetsOnEmpty && it->second.empty()) { - sendSearchSetsRequest(query); - return; - } - toggleSearchLoading(false); - if (requestSetsOnEmpty) { + finishLoading(); + if (isInitial) { showSearchResults(); } else { refreshSearchRows(); @@ -1246,10 +1246,10 @@ void StickersListWidget::checkPaginateSearchStickers( void StickersListWidget::searchResultsDone( const QString &query, const MTPmessages_FoundStickerSets &result) { - if (_searchNextQuery == query) { + _searchSetsRequestId = 0; + if (_searchNextQuery == query && !_searchStickersRequestId) { toggleSearchLoading(false); } - _searchSetsRequestId = 0; result.match([&](const MTPDmessages_foundStickerSetsNotModified &data) { LOG(("API Error: " diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index d6a842eef1..724b58ead2 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -388,11 +388,11 @@ private: void requestSearchStickers( const QString &query, int offset, - bool requestSetsOnEmpty); + bool isInitial); void searchStickersResultsDone( const QString &query, int requestedOffset, - bool requestSetsOnEmpty, + bool isInitial, const MTPmessages_FoundStickers &result); void loadMoreSearchStickers(); void checkPaginateSearchStickers(int visibleTop, int visibleBottom); From 22888284cb5afd6b8351c16b1fac62dc604a1b52 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 13:05:29 +0300 Subject: [PATCH 027/154] Added cover document of single-cover sticker sets when empty. --- Telegram/SourceFiles/data/stickers/data_stickers.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index 8c58d8cef9..9850b6623e 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -1658,7 +1658,12 @@ not_null Stickers::feedSet( const auto set = data.match([&](const auto &data) { return feedSet(data.vset()); }); - data.match([](const MTPDstickerSetCovered &data) { + data.match([&](const MTPDstickerSetCovered &data) { + set->covers = StickersPack(); + const auto cover = session().data().processDocument(data.vcover()); + if (cover->sticker()) { + set->covers.push_back(cover); + } }, [&](const MTPDstickerSetNoCovered &data) { }, [&](const MTPDstickerSetMultiCovered &data) { feedSetCovers(set, data.vcovers().v); From f226f79f2afe50165bb9374457403a336deef9e4 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 13:09:45 +0300 Subject: [PATCH 028/154] Centered short pack titles in shortcut tiles in emoji/stickers lists. --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 5 ++++- Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index b3b0e354b2..e3fe10ed36 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -2108,10 +2108,13 @@ void EmojiListWidget::paintSearchShortcuts(Painter &p, QRect clip) { title = st::normalFont->elided(title, available); titleWidth = st::normalFont->width(title); } + const auto titleLeft = (titleWidth < available) + ? (rect.x() + (rect.width() - titleWidth) / 2) + : (rect.x() + st().searchPackTextPadding); p.setFont(st::normalFont); p.setPen(st().textFg); p.drawTextLeft( - rect.x() + st().searchPackTextPadding, + titleLeft, rect.y() + st().searchPackTextTop, width(), title, diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 47593dae6f..b294fce625 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -1390,10 +1390,13 @@ void StickersListWidget::paintSearchShortcuts(Painter &p, QRect clip) { title = st::normalFont->elided(title, available); titleWidth = st::normalFont->width(title); } + const auto titleLeft = (titleWidth < available) + ? (rect.x() + (rect.width() - titleWidth) / 2) + : (rect.x() + st().searchPackTextPadding); p.setFont(st::normalFont); p.setPen(st().textFg); p.drawTextLeft( - rect.x() + st().searchPackTextPadding, + titleLeft, rect.y() + st().searchPackTextTop, width(), title, From 4c26b84dcb0d1a6cfd511c47bda974b6689b5aed Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 13:12:38 +0300 Subject: [PATCH 029/154] Added horizontal wheel scrolling for shortcut pack strip. --- .../chat_helpers/emoji_list_widget.cpp | 23 +++++++++++++++++++ .../chat_helpers/emoji_list_widget.h | 1 + .../chat_helpers/stickers_list_widget.cpp | 23 +++++++++++++++++++ .../chat_helpers/stickers_list_widget.h | 1 + 4 files changed, 48 insertions(+) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index e3fe10ed36..60abedc4a8 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -3111,6 +3111,29 @@ void EmojiListWidget::colorChosen(EmojiChosen data) { _picker->hideAnimated(); } +void EmojiListWidget::wheelEvent(QWheelEvent *e) { + if (searchShortcutsShown() && _searchShortcutsScrollMax > 0) { + const auto pos = mapFromGlobal(e->globalPosition().toPoint()); + if (pos.y() >= searchShortcutsTop() + && pos.y() < searchShortcutsTop() + searchShortcutsHeight()) { + const auto angle = e->angleDelta(); + const auto pixel = e->pixelDelta(); + const auto horizontal = (angle.x() != 0); + const auto vertical = (angle.y() != 0); + if (horizontal || vertical) { + const auto delta = horizontal + ? ((rtl() ? -1 : 1) + * (pixel.x() ? pixel.x() : angle.x())) + : (pixel.y() ? pixel.y() : angle.y()); + scrollSearchShortcutsTo(_searchShortcutsScroll - delta); + e->accept(); + return; + } + } + } + Inner::wheelEvent(e); +} + void EmojiListWidget::mouseMoveEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); if (std::get_if(&_pressed) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 586518ce05..38c31560e1 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -170,6 +170,7 @@ protected: void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; + void wheelEvent(QWheelEvent *e) override; void paintEvent(QPaintEvent *e) override; void leaveEventHook(QEvent *e) override; void leaveToChildEvent(QEvent *e, QWidget *child) override; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index b294fce625..eaf7978bd0 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -2704,6 +2704,29 @@ void StickersListWidget::setColumnCount(int count) { } } +void StickersListWidget::wheelEvent(QWheelEvent *e) { + if (searchShortcutsShown() && _searchShortcutsScrollMax > 0) { + const auto pos = mapFromGlobal(e->globalPosition().toPoint()); + if (pos.y() >= searchShortcutsTop() + && pos.y() < searchShortcutsTop() + searchShortcutsHeight()) { + const auto angle = e->angleDelta(); + const auto pixel = e->pixelDelta(); + const auto horizontal = (angle.x() != 0); + const auto vertical = (angle.y() != 0); + if (horizontal || vertical) { + const auto delta = horizontal + ? ((rtl() ? -1 : 1) + * (pixel.x() ? pixel.x() : angle.x())) + : (pixel.y() ? pixel.y() : angle.y()); + scrollSearchShortcutsTo(_searchShortcutsScroll - delta); + e->accept(); + return; + } + } + } + Inner::wheelEvent(e); +} + void StickersListWidget::mouseMoveEvent(QMouseEvent *e) { _lastMousePosition = e->globalPos(); if (std::get_if(&_pressed) diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index 724b58ead2..f1bfefbbeb 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -144,6 +144,7 @@ protected: void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; + void wheelEvent(QWheelEvent *e) override; void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; void leaveEventHook(QEvent *e) override; From 3498799376c6f50d6ff88e9dc89ef619ef7a654e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 13:31:37 +0300 Subject: [PATCH 030/154] Preserved shortcut ripple animation across pack rebuild. --- .../SourceFiles/chat_helpers/emoji_list_widget.cpp | 11 ++++++++++- .../chat_helpers/stickers_list_widget.cpp | 12 ++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 60abedc4a8..cfd4223783 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1074,10 +1074,19 @@ void EmojiListWidget::showSearchResults() { _searchResults.clear(); _searchCustomIds.clear(); _searchSets.clear(); - _searchShortcutSets.clear(); + auto wasShortcuts = base::take(_searchShortcutSets); _searchEmoji.clear(); refreshSearchShortcuts(); + for (auto &set : _searchShortcutSets) { + const auto i = ranges::find( + wasShortcuts, + set.id, + &CustomSet::id); + if (i != wasShortcuts.end() && i->ripple) { + set.ripple = std::move(i->ripple); + } + } if (searchShortcutSelected()) { fillSelectedSearchShortcut(); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index eaf7978bd0..41e4936f4c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -771,6 +771,18 @@ void StickersListWidget::refreshSearchRows( if (_section == wasSection && _section == Section::Search) { takeHeavyData(_searchSets, wasSets); takeHeavyData(_searchShortcutSets, wasShortcuts); + auto indices = base::flat_map(); + indices.reserve(wasShortcuts.size()); + auto index = 0; + for (const auto &set : wasShortcuts) { + indices.emplace(set.id, index++); + } + for (auto &set : _searchShortcutSets) { + const auto i = indices.find(set.id); + if (i != end(indices)) { + set.ripple = std::move(wasShortcuts[i->second].ripple); + } + } } }); From 8b206ce27813bcb408c77c9fe89d7830a5dff91c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 13:40:07 +0300 Subject: [PATCH 031/154] Animated back-to-search row appearance with slide-wrap timing. --- .../chat_helpers/chat_helpers.style | 2 + .../chat_helpers/emoji_list_widget.cpp | 74 +++++++++++++++---- .../chat_helpers/emoji_list_widget.h | 6 ++ .../chat_helpers/stickers_list_widget.cpp | 74 +++++++++++++++---- .../chat_helpers/stickers_list_widget.h | 6 ++ 5 files changed, 136 insertions(+), 26 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 1b45f2ea99..dc1dbeadab 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -133,6 +133,7 @@ EmojiPan { searchPacksBottom: pixels; searchResultsHeight: pixels; searchResultsTextTop: pixels; + searchSwapDuration: int; searchBackHeight: pixels; searchBackIconLeft: pixels; searchBackIconTop: pixels; @@ -797,6 +798,7 @@ defaultEmojiPan: EmojiPan { searchPacksBottom: 0px; searchResultsHeight: 20px; searchResultsTextTop: 3px; + searchSwapDuration: 100; searchBackHeight: 36px; searchBackIconLeft: 5px; searchBackIconTop: 6px; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index cfd4223783..a8481806a0 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1269,6 +1269,35 @@ bool EmojiListWidget::searchShortcutSelected() const { return _searchSelectedSetId != 0; } +void EmojiListWidget::startSearchSwapAnimation(Fn change) { + if (!isVisible() || size().isEmpty()) { + change(); + return; + } + const auto computeRect = [&] { + const auto top = searchShortcutsTop(); + const auto bottom = std::max(top + 1, getVisibleBottom()); + return QRect(0, top, width(), bottom - top); + }; + _searchSwapAnimation.stop(); + _searchSwapBefore = Ui::GrabWidget(this, computeRect()); + _searchSwapTop = searchShortcutsTop(); + change(); + _searchSwapAfter = Ui::GrabWidget(this, computeRect()); + _searchSwapAnimation.start( + [=, this] { + update(); + if (!_searchSwapAnimation.animating()) { + _searchSwapBefore = QPixmap(); + _searchSwapAfter = QPixmap(); + } + }, + 0., + 1., + st().searchSwapDuration, + anim::sineInOut); +} + int EmojiListWidget::searchShortcutsTop() const { return _search ? _search->height() : 0; } @@ -1280,11 +1309,9 @@ int EmojiListWidget::searchShortcutsHeight() const { auto result = st().searchPacksTop + st().searchPackHeight + st().searchPacksBottom; - if (searchShortcutSelected()) { - result += st().searchBackHeight; - } else { - result += st().searchResultsHeight; - } + result += searchShortcutSelected() + ? st().searchBackHeight + : st().searchResultsHeight; return result; } @@ -1346,16 +1373,21 @@ void EmojiListWidget::toggleSearchShortcut(int index) { return; } const auto setId = _searchShortcutSets[index].id; - _searchSelectedSetId = (_searchSelectedSetId == setId) ? 0 : setId; - showSearchResults(); + const auto target = (_searchSelectedSetId == setId) ? 0 : setId; + startSearchSwapAnimation([=, this] { + _searchSelectedSetId = target; + showSearchResults(); + }); } void EmojiListWidget::backToSearchResults() { if (!_searchSelectedSetId) { return; } - _searchSelectedSetId = 0; - showSearchResults(); + startSearchSwapAnimation([=, this] { + _searchSelectedSetId = 0; + showSearchResults(); + }); } EmojiListWidget::CustomSet &EmojiListWidget::searchSetBySection( @@ -1999,6 +2031,23 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) { _searchExpandCache = QImage(); } + if (_searchSwapAnimation.animating()) { + const auto progress = _searchSwapAnimation.value(1.); + const auto slide = st().searchBackHeight; + p.setOpacity(1. - progress); + p.drawPixmap( + 0, + _searchSwapTop + int(base::SafeRound(slide * progress)), + _searchSwapBefore); + p.setOpacity(progress); + p.drawPixmap( + 0, + _searchSwapTop - int(base::SafeRound(slide * (1. - progress))), + _searchSwapAfter); + p.setOpacity(1.); + return; + } + paint(p, {}, clip); } @@ -2035,8 +2084,8 @@ void EmojiListWidget::paintSearchShortcuts(Painter &p, QRect clip) { || clip.top() >= searchShortcutsTop() + searchShortcutsHeight()) { return; } - if (searchShortcutSelected()) { - const auto back = searchBackRect(); + const auto back = searchBackRect(); + if (back.height() > 0) { const auto selected = std::get_if( !v::is_null(_pressed) ? &_pressed : &_selected); const auto &icon = selected @@ -2074,8 +2123,7 @@ void EmojiListWidget::paintSearchShortcuts(Painter &p, QRect clip) { p.setClipRect( QRect( 0, - searchShortcutsTop() - + (searchShortcutSelected() ? st().searchBackHeight : 0), + searchShortcutsTop() + back.height(), width(), st().searchPacksTop + st().searchPackHeight diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 38c31560e1..198220e343 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" +#include "ui/effects/animations.h" #include "ui/widgets/tooltip.h" #include "ui/round_rect.h" #include "base/timer.h" @@ -347,6 +348,7 @@ private: void fillSelectedSearchShortcut(); [[nodiscard]] bool searchShortcutsShown() const; [[nodiscard]] bool searchShortcutSelected() const; + void startSearchSwapAnimation(Fn change); [[nodiscard]] int searchShortcutsHeight() const; [[nodiscard]] int searchShortcutsTop() const; [[nodiscard]] QRect searchBackRect() const; @@ -547,6 +549,10 @@ private: int _searchShortcutsDragStart = 0; QPoint _searchShortcutsMouseDown; bool _searchShortcutsDragging = false; + Ui::Animations::Simple _searchSwapAnimation; + QPixmap _searchSwapBefore; + QPixmap _searchSwapAfter; + int _searchSwapTop = 0; mtpRequestId _searchCloudRequestId = 0; mtpRequestId _searchSetsRequestId = 0; bool _searchLoading = false; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 41e4936f4c..d83828dbc5 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/image/image.h" #include "ui/cached_round_corners.h" #include "ui/power_saving.h" +#include "ui/ui_utility.h" #include "lottie/lottie_multi_player.h" #include "lottie/lottie_single_player.h" #include "lottie/lottie_animation.h" @@ -920,6 +921,35 @@ bool StickersListWidget::searchShortcutSelected() const { return _searchSelectedSetId != 0; } +void StickersListWidget::startSearchSwapAnimation(Fn change) { + if (!isVisible() || size().isEmpty()) { + change(); + return; + } + const auto computeRect = [&] { + const auto top = searchShortcutsTop(); + const auto bottom = std::max(top + 1, getVisibleBottom()); + return QRect(0, top, width(), bottom - top); + }; + _searchSwapAnimation.stop(); + _searchSwapBefore = Ui::GrabWidget(this, computeRect()); + _searchSwapTop = searchShortcutsTop(); + change(); + _searchSwapAfter = Ui::GrabWidget(this, computeRect()); + _searchSwapAnimation.start( + [=, this] { + update(); + if (!_searchSwapAnimation.animating()) { + _searchSwapBefore = QPixmap(); + _searchSwapAfter = QPixmap(); + } + }, + 0., + 1., + st().searchSwapDuration, + anim::sineInOut); +} + int StickersListWidget::searchShortcutsTop() const { return _search ? _search->height() : 0; } @@ -931,11 +961,9 @@ int StickersListWidget::searchShortcutsHeight() const { auto result = st().searchPacksTop + st().searchPackHeight + st().searchPacksBottom; - if (searchShortcutSelected()) { - result += st().searchBackHeight; - } else { - result += st().searchResultsHeight; - } + result += searchShortcutSelected() + ? st().searchBackHeight + : st().searchResultsHeight; return result; } @@ -997,16 +1025,21 @@ void StickersListWidget::toggleSearchShortcut(int index) { return; } const auto setId = _searchShortcutSets[index].id; - _searchSelectedSetId = (_searchSelectedSetId == setId) ? 0 : setId; - showSearchResults(); + const auto target = (_searchSelectedSetId == setId) ? 0 : setId; + startSearchSwapAnimation([=, this] { + _searchSelectedSetId = target; + showSearchResults(); + }); } void StickersListWidget::backToSearchResults() { if (!_searchSelectedSetId) { return; } - _searchSelectedSetId = 0; - showSearchResults(); + startSearchSwapAnimation([=, this] { + _searchSelectedSetId = 0; + showSearchResults(); + }); } void StickersListWidget::fillFoundStickersRow( @@ -1311,6 +1344,22 @@ void StickersListWidget::paintEvent(QPaintEvent *e) { p.fillRect(clip, st().bg); } + if (_searchSwapAnimation.animating()) { + const auto progress = _searchSwapAnimation.value(1.); + const auto slide = st().searchBackHeight; + p.setOpacity(1. - progress); + p.drawPixmap( + 0, + _searchSwapTop + int(base::SafeRound(slide * progress)), + _searchSwapBefore); + p.setOpacity(progress); + p.drawPixmap( + 0, + _searchSwapTop - int(base::SafeRound(slide * (1. - progress))), + _searchSwapAfter); + p.setOpacity(1.); + return; + } paintStickers(p, clip); } @@ -1320,8 +1369,8 @@ void StickersListWidget::paintSearchShortcuts(Painter &p, QRect clip) { || clip.top() >= searchShortcutsTop() + searchShortcutsHeight()) { return; } - if (searchShortcutSelected()) { - const auto back = searchBackRect(); + const auto back = searchBackRect(); + if (back.height() > 0) { const auto selected = std::get_if( !v::is_null(_pressed) ? &_pressed : &_selected); const auto &icon = selected @@ -1359,8 +1408,7 @@ void StickersListWidget::paintSearchShortcuts(Painter &p, QRect clip) { p.setClipRect( QRect( 0, - searchShortcutsTop() - + (searchShortcutSelected() ? st().searchBackHeight : 0), + searchShortcutsTop() + back.height(), width(), st().searchPacksTop + st().searchPackHeight diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index f1bfefbbeb..9b4834a063 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" #include "data/stickers/data_stickers.h" +#include "ui/effects/animations.h" #include "ui/round_rect.h" #include "base/variant.h" #include "base/timer.h" @@ -407,6 +408,7 @@ private: void fillSelectedSearchShortcut(); [[nodiscard]] bool searchShortcutsShown() const; [[nodiscard]] bool searchShortcutSelected() const; + void startSearchSwapAnimation(Fn change); [[nodiscard]] int searchShortcutsHeight() const; [[nodiscard]] int searchShortcutsTop() const; [[nodiscard]] QRect searchBackRect() const; @@ -512,6 +514,10 @@ private: int _searchShortcutsDragStart = 0; QPoint _searchShortcutsMouseDown; bool _searchShortcutsDragging = false; + Ui::Animations::Simple _searchSwapAnimation; + QPixmap _searchSwapBefore; + QPixmap _searchSwapAfter; + int _searchSwapTop = 0; mtpRequestId _searchSetsRequestId = 0; mtpRequestId _searchStickersRequestId = 0; bool _searchLoading = false; From e090798db58586f0c53bea8e9b2be051368caf58 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 15:29:07 +0300 Subject: [PATCH 032/154] Reversed swap animation direction when leaving selected pack. --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 7 +++++-- Telegram/SourceFiles/chat_helpers/emoji_list_widget.h | 1 + Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp | 7 +++++-- Telegram/SourceFiles/chat_helpers/stickers_list_widget.h | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index a8481806a0..d23a80df55 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1280,9 +1280,11 @@ void EmojiListWidget::startSearchSwapAnimation(Fn change) { return QRect(0, top, width(), bottom - top); }; _searchSwapAnimation.stop(); + const auto wasSelected = searchShortcutSelected(); _searchSwapBefore = Ui::GrabWidget(this, computeRect()); _searchSwapTop = searchShortcutsTop(); change(); + _searchSwapReverse = wasSelected && !searchShortcutSelected(); _searchSwapAfter = Ui::GrabWidget(this, computeRect()); _searchSwapAnimation.start( [=, this] { @@ -2033,16 +2035,17 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) { if (_searchSwapAnimation.animating()) { const auto progress = _searchSwapAnimation.value(1.); + const auto direction = _searchSwapReverse ? -1 : 1; const auto slide = st().searchBackHeight; p.setOpacity(1. - progress); p.drawPixmap( 0, - _searchSwapTop + int(base::SafeRound(slide * progress)), + _searchSwapTop + direction * int(base::SafeRound(slide * progress)), _searchSwapBefore); p.setOpacity(progress); p.drawPixmap( 0, - _searchSwapTop - int(base::SafeRound(slide * (1. - progress))), + _searchSwapTop - direction * int(base::SafeRound(slide * (1. - progress))), _searchSwapAfter); p.setOpacity(1.); return; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 198220e343..eadfb69bcd 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -553,6 +553,7 @@ private: QPixmap _searchSwapBefore; QPixmap _searchSwapAfter; int _searchSwapTop = 0; + bool _searchSwapReverse = false; mtpRequestId _searchCloudRequestId = 0; mtpRequestId _searchSetsRequestId = 0; bool _searchLoading = false; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index d83828dbc5..6c1a392d7c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -932,9 +932,11 @@ void StickersListWidget::startSearchSwapAnimation(Fn change) { return QRect(0, top, width(), bottom - top); }; _searchSwapAnimation.stop(); + const auto wasSelected = searchShortcutSelected(); _searchSwapBefore = Ui::GrabWidget(this, computeRect()); _searchSwapTop = searchShortcutsTop(); change(); + _searchSwapReverse = wasSelected && !searchShortcutSelected(); _searchSwapAfter = Ui::GrabWidget(this, computeRect()); _searchSwapAnimation.start( [=, this] { @@ -1346,16 +1348,17 @@ void StickersListWidget::paintEvent(QPaintEvent *e) { if (_searchSwapAnimation.animating()) { const auto progress = _searchSwapAnimation.value(1.); + const auto direction = _searchSwapReverse ? -1 : 1; const auto slide = st().searchBackHeight; p.setOpacity(1. - progress); p.drawPixmap( 0, - _searchSwapTop + int(base::SafeRound(slide * progress)), + _searchSwapTop + direction * int(base::SafeRound(slide * progress)), _searchSwapBefore); p.setOpacity(progress); p.drawPixmap( 0, - _searchSwapTop - int(base::SafeRound(slide * (1. - progress))), + _searchSwapTop - direction * int(base::SafeRound(slide * (1. - progress))), _searchSwapAfter); p.setOpacity(1.); return; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index 9b4834a063..c844bcc091 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -518,6 +518,7 @@ private: QPixmap _searchSwapBefore; QPixmap _searchSwapAfter; int _searchSwapTop = 0; + bool _searchSwapReverse = false; mtpRequestId _searchSetsRequestId = 0; mtpRequestId _searchStickersRequestId = 0; bool _searchLoading = false; From d3d30c68fd0e282e7e29d852ef9cbd47422643ac Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 14:42:06 +0300 Subject: [PATCH 033/154] Centered and enlarged emoji preview in shortcut tile in emoji list. --- .../SourceFiles/chat_helpers/emoji_list_widget.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index d23a80df55..958da83b59 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -2201,22 +2201,29 @@ void EmojiListWidget::paintSearchShortcutIcon( Painter &p, const CustomSet &set, QRect rect) { - if (set.list.empty()) { + if (set.list.empty() || _customSingleSize <= 0) { return; } + const auto native = _customSingleSize; + const auto scale = double(rect.width()) / double(native); auto context = Ui::Text::CustomEmojiPaintContext{ .textColor = (_customTextColor ? _customTextColor() : st().textFg->c), - .size = rect.size(), + .size = QSize(native, native), .now = crl::now(), .scale = 1., - .position = rect.topLeft(), + .position = QPoint(), .paused = On(powerSavingFlag()) || paused(), .scaled = false, .internal = { .forceFirstFrame = true }, }; + p.save(); + p.translate(rect.center()); + p.scale(scale, scale); + p.translate(-native / 2, -native / 2); set.list.front().custom->paint(p, context); + p.restore(); } void EmojiListWidget::paint( From 3957670e9f484a095a8eac253a9c47696d90552b Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 14:51:22 +0300 Subject: [PATCH 034/154] Kept emoji search state across tab switches by skipping cancel. --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 958da83b59..c6b4d318fa 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -3305,12 +3305,17 @@ void EmojiListWidget::processHideFinished() { _picker->hideFast(); _pickerSelected = v::null; } - cancelSearchRequest(); unloadAllCustom(); clearSelection(); } void EmojiListWidget::processPanelHideFinished() { + if (_search) { + _search->cancel(); + } + _nextSearchQuery.clear(); + applyNextSearchQuery(); + cancelSearchRequest(); unloadAllCustom(); if (_localSetsManager->clearInstalledLocally()) { refreshCustom(); From d71f70d0f2105dee0049d08ab6ea646444212f4e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 15:03:55 +0300 Subject: [PATCH 035/154] Limited swap animation to content area on pack-to-pack switch. --- .../SourceFiles/chat_helpers/emoji_list_widget.cpp | 14 ++++++++++---- .../SourceFiles/chat_helpers/emoji_list_widget.h | 2 +- .../chat_helpers/stickers_list_widget.cpp | 14 ++++++++++---- .../chat_helpers/stickers_list_widget.h | 2 +- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index c6b4d318fa..0b0ce1d4c1 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1269,20 +1269,23 @@ bool EmojiListWidget::searchShortcutSelected() const { return _searchSelectedSetId != 0; } -void EmojiListWidget::startSearchSwapAnimation(Fn change) { +void EmojiListWidget::startSearchSwapAnimation( + Fn change, + bool packToPack) { if (!isVisible() || size().isEmpty()) { change(); return; } + const auto top = searchShortcutsTop() + + (packToPack ? searchShortcutsHeight() : 0); const auto computeRect = [&] { - const auto top = searchShortcutsTop(); const auto bottom = std::max(top + 1, getVisibleBottom()); return QRect(0, top, width(), bottom - top); }; _searchSwapAnimation.stop(); const auto wasSelected = searchShortcutSelected(); _searchSwapBefore = Ui::GrabWidget(this, computeRect()); - _searchSwapTop = searchShortcutsTop(); + _searchSwapTop = top; change(); _searchSwapReverse = wasSelected && !searchShortcutSelected(); _searchSwapAfter = Ui::GrabWidget(this, computeRect()); @@ -1376,10 +1379,13 @@ void EmojiListWidget::toggleSearchShortcut(int index) { } const auto setId = _searchShortcutSets[index].id; const auto target = (_searchSelectedSetId == setId) ? 0 : setId; + const auto packToPack = _searchSelectedSetId + && target + && _searchSelectedSetId != target; startSearchSwapAnimation([=, this] { _searchSelectedSetId = target; showSearchResults(); - }); + }, packToPack); } void EmojiListWidget::backToSearchResults() { diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index eadfb69bcd..ae3b6cca2b 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -348,7 +348,7 @@ private: void fillSelectedSearchShortcut(); [[nodiscard]] bool searchShortcutsShown() const; [[nodiscard]] bool searchShortcutSelected() const; - void startSearchSwapAnimation(Fn change); + void startSearchSwapAnimation(Fn change, bool packToPack = false); [[nodiscard]] int searchShortcutsHeight() const; [[nodiscard]] int searchShortcutsTop() const; [[nodiscard]] QRect searchBackRect() const; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 6c1a392d7c..4077011730 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -921,20 +921,23 @@ bool StickersListWidget::searchShortcutSelected() const { return _searchSelectedSetId != 0; } -void StickersListWidget::startSearchSwapAnimation(Fn change) { +void StickersListWidget::startSearchSwapAnimation( + Fn change, + bool packToPack) { if (!isVisible() || size().isEmpty()) { change(); return; } + const auto top = searchShortcutsTop() + + (packToPack ? searchShortcutsHeight() : 0); const auto computeRect = [&] { - const auto top = searchShortcutsTop(); const auto bottom = std::max(top + 1, getVisibleBottom()); return QRect(0, top, width(), bottom - top); }; _searchSwapAnimation.stop(); const auto wasSelected = searchShortcutSelected(); _searchSwapBefore = Ui::GrabWidget(this, computeRect()); - _searchSwapTop = searchShortcutsTop(); + _searchSwapTop = top; change(); _searchSwapReverse = wasSelected && !searchShortcutSelected(); _searchSwapAfter = Ui::GrabWidget(this, computeRect()); @@ -1028,10 +1031,13 @@ void StickersListWidget::toggleSearchShortcut(int index) { } const auto setId = _searchShortcutSets[index].id; const auto target = (_searchSelectedSetId == setId) ? 0 : setId; + const auto packToPack = _searchSelectedSetId + && target + && _searchSelectedSetId != target; startSearchSwapAnimation([=, this] { _searchSelectedSetId = target; showSearchResults(); - }); + }, packToPack); } void StickersListWidget::backToSearchResults() { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index c844bcc091..ef9c002f02 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -408,7 +408,7 @@ private: void fillSelectedSearchShortcut(); [[nodiscard]] bool searchShortcutsShown() const; [[nodiscard]] bool searchShortcutSelected() const; - void startSearchSwapAnimation(Fn change); + void startSearchSwapAnimation(Fn change, bool packToPack = false); [[nodiscard]] int searchShortcutsHeight() const; [[nodiscard]] int searchShortcutsTop() const; [[nodiscard]] QRect searchBackRect() const; From 4187c8787651db9f75431a7e9a9a260ac21f7415 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 15:13:03 +0300 Subject: [PATCH 036/154] Skipped strip area for animation during pack-to-pack swap animation. --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp | 4 ++++ Telegram/SourceFiles/chat_helpers/emoji_list_widget.h | 1 + Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp | 4 ++++ Telegram/SourceFiles/chat_helpers/stickers_list_widget.h | 1 + 4 files changed, 10 insertions(+) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 0b0ce1d4c1..7146d5ee66 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1286,6 +1286,7 @@ void EmojiListWidget::startSearchSwapAnimation( const auto wasSelected = searchShortcutSelected(); _searchSwapBefore = Ui::GrabWidget(this, computeRect()); _searchSwapTop = top; + _searchSwapPartial = packToPack; change(); _searchSwapReverse = wasSelected && !searchShortcutSelected(); _searchSwapAfter = Ui::GrabWidget(this, computeRect()); @@ -2040,6 +2041,9 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) { } if (_searchSwapAnimation.animating()) { + if (_searchSwapPartial) { + paint(p, {}, clip); + } const auto progress = _searchSwapAnimation.value(1.); const auto direction = _searchSwapReverse ? -1 : 1; const auto slide = st().searchBackHeight; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index ae3b6cca2b..935d41fe0f 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -554,6 +554,7 @@ private: QPixmap _searchSwapAfter; int _searchSwapTop = 0; bool _searchSwapReverse = false; + bool _searchSwapPartial = false; mtpRequestId _searchCloudRequestId = 0; mtpRequestId _searchSetsRequestId = 0; bool _searchLoading = false; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 4077011730..029d5e8ef4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -938,6 +938,7 @@ void StickersListWidget::startSearchSwapAnimation( const auto wasSelected = searchShortcutSelected(); _searchSwapBefore = Ui::GrabWidget(this, computeRect()); _searchSwapTop = top; + _searchSwapPartial = packToPack; change(); _searchSwapReverse = wasSelected && !searchShortcutSelected(); _searchSwapAfter = Ui::GrabWidget(this, computeRect()); @@ -1353,6 +1354,9 @@ void StickersListWidget::paintEvent(QPaintEvent *e) { } if (_searchSwapAnimation.animating()) { + if (_searchSwapPartial) { + paintStickers(p, clip); + } const auto progress = _searchSwapAnimation.value(1.); const auto direction = _searchSwapReverse ? -1 : 1; const auto slide = st().searchBackHeight; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index ef9c002f02..dc32b873fd 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -519,6 +519,7 @@ private: QPixmap _searchSwapAfter; int _searchSwapTop = 0; bool _searchSwapReverse = false; + bool _searchSwapPartial = false; mtpRequestId _searchSetsRequestId = 0; mtpRequestId _searchStickersRequestId = 0; bool _searchLoading = false; From 8f14d43eedd366291c4a249ecda3bc3611880fd2 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 18:27:34 +0300 Subject: [PATCH 037/154] Removed display of notification action buttons under hidden preview. --- Telegram/SourceFiles/window/notifications_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 6aec58db09..055a2de35b 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1614,7 +1614,7 @@ void NativeManager::doShowNotification(NotificationFields &&fields) { }); } : Fn(); auto actions = std::vector(); - if (AllowNotificationActions(peer)) { + if (AllowNotificationActions(peer) && !options.hideMarkAsRead) { if (const auto markup = item->inlineReplyMarkup()) { using ButtonType = HistoryMessageMarkupButton::Type; const auto &rows = markup->data.rows; From cb7e811ed17297cb5eaef882dda5bd62494e3d72 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 18:31:09 +0300 Subject: [PATCH 038/154] Guarded sender userpic moderate handler against item deletion. --- Telegram/SourceFiles/window/window_peer_menu.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index ef397ddc08..e1c095eae9 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -4097,13 +4097,19 @@ void AddSenderUserpicModerateAction( && CanCreateModerateMessagesBox( HistoryItemsList{ not_null(moderateItem) }); if (canDeleteAndBan) { + const auto itemId = moderateItem->fullId(); addAction({ .isSeparator = true }); addAction({ .text = tr::lng_context_delete_and_ban(tr::now), .handler = [=] { + const auto item = controller->session().data().message( + itemId); + if (!item) { + return; + } controller->show(Box( CreateModerateMessagesBox, - HistoryItemsList{ not_null(moderateItem) }, + HistoryItemsList{ not_null(item) }, nullptr, ModerateMessagesBoxOptions{ .reportSpam = true, From 5854ba7c3ce32999344b5e578075312f5b769c69 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Apr 2026 18:36:26 +0300 Subject: [PATCH 039/154] Encoded OAuth tokens before concatenating into tg:// deep links. --- Telegram/SourceFiles/core/local_url_handlers.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index b3ea2eedb6..693b19c6f1 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -550,7 +550,8 @@ bool ResolveUsernameOrPhone( UrlAuthBox::ActivateUrl( controller->uiShow(), &controller->session(), - u"tg://resolve?domain=oauth&startapp="_q + token, + u"tg://resolve?domain=oauth&startapp="_q + + qthelp::url_encode(token), context); return true; } @@ -1629,7 +1630,7 @@ bool ResolveOAuth( UrlAuthBox::ActivateUrl( controller->uiShow(), &controller->session(), - u"tg://oauth?token="_q + token, + u"tg://oauth?token="_q + qthelp::url_encode(token), context); return true; } From c8f316302b47919a70fbfa73a09c2606580e0949 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 28 Apr 2026 08:38:10 +0300 Subject: [PATCH 040/154] [img-editor] Split crop shape role into visual hint and output mask. --- .../SourceFiles/boxes/sticker_creator_box.cpp | 7 +++---- Telegram/SourceFiles/editor/photo_editor.cpp | 1 + .../SourceFiles/editor/photo_editor_common.cpp | 18 +++++++----------- .../SourceFiles/editor/photo_editor_common.h | 12 ++++++++---- .../info/profile/info_profile_top_bar.cpp | 5 ++++- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/boxes/sticker_creator_box.cpp b/Telegram/SourceFiles/boxes/sticker_creator_box.cpp index 6c31f2887d..4a992173ec 100644 --- a/Telegram/SourceFiles/boxes/sticker_creator_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_creator_box.cpp @@ -148,6 +148,7 @@ void OpenPhotoEditorForSticker( Editor::EditorData{ .exactSize = QSize(kStickerSide, kStickerSide), .cropType = Editor::EditorData::CropType::RoundedRect, + .cropMode = Editor::EditorData::CropMode::Mask, .keepAspectRatio = true, .fixedCrop = true, }); @@ -155,9 +156,7 @@ void OpenPhotoEditorForSticker( auto applyModifications = [=, done = std::move(onDone)]( const Editor::PhotoModifications &mods) mutable { - auto unmasked = mods; - unmasked.cropType = Editor::EditorData::CropType::Rect; - auto result = Editor::ImageModified(baseImage->original(), unmasked); + auto result = Editor::ImageModified(baseImage->original(), mods); if (result.size() != QSize(kStickerSide, kStickerSide)) { result = result.scaled( kStickerSide, @@ -165,7 +164,7 @@ void OpenPhotoEditorForSticker( Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } - Editor::ApplyShapeMask(result, mods.cropType, mods.cornersLevel); + Editor::ApplyShapeMask(result, mods); done(std::move(result)); }; diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index 50476bcf07..e117008131 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -238,6 +238,7 @@ PhotoEditor::PhotoEditor( _brushes, _brushTool)) { _modifications.cropType = data.cropType; + _modifications.cropMode = data.cropMode; sizeValue( ) | rpl::on_next([=](const QSize &size) { diff --git a/Telegram/SourceFiles/editor/photo_editor_common.cpp b/Telegram/SourceFiles/editor/photo_editor_common.cpp index 0a098f98f7..18f1833a61 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_common.cpp @@ -13,15 +13,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Editor { -void ApplyShapeMask( - QImage &image, - EditorData::CropType type, - RoundedCornersLevel cornersLevel) { +void ApplyShapeMask(QImage &image, const PhotoModifications &mods) { + if (mods.cropMode != EditorData::CropMode::Mask) { + return; + } + const auto type = mods.cropType; if (type == EditorData::CropType::Rect) { return; } const auto multiplier = (type == EditorData::CropType::RoundedRect) - ? RoundedCornersMultiplier(cornersLevel) + ? RoundedCornersMultiplier(mods.cornersLevel) : Ui::ForumUserpicRadiusMultiplier(); if (type == EditorData::CropType::RoundedRect && multiplier <= 0.) { return; @@ -80,7 +81,6 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) { auto cropped = mods.crop.isValid() ? image.copy(mods.crop) : image; - ApplyShapeMask(cropped, mods.cropType, mods.cornersLevel); QTransform transform; if (mods.flipped) { transform.scale(-1, 1); @@ -92,11 +92,7 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) { } bool PhotoModifications::empty() const { - return !angle - && !flipped - && !crop.isValid() - && cropType == EditorData::CropType::Rect - && !paint; + return !angle && !flipped && !crop.isValid() && !paint; } PhotoModifications::operator bool() const { diff --git a/Telegram/SourceFiles/editor/photo_editor_common.h b/Telegram/SourceFiles/editor/photo_editor_common.h index 59354f0fc8..5695b6dc12 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.h +++ b/Telegram/SourceFiles/editor/photo_editor_common.h @@ -27,10 +27,16 @@ struct EditorData { RoundedRect, }; + enum class CropMode { + Hint, + Mask, + }; + TextWithEntities about; QString confirm; QSize exactSize; CropType cropType = CropType::Rect; + CropMode cropMode = CropMode::Hint; bool keepAspectRatio = false; bool fixedCrop = false; }; @@ -40,6 +46,7 @@ struct PhotoModifications { bool flipped = false; QRect crop; EditorData::CropType cropType = EditorData::CropType::Rect; + EditorData::CropMode cropMode = EditorData::CropMode::Hint; RoundedCornersLevel cornersLevel = RoundedCornersLevel::Large; std::shared_ptr paint = nullptr; @@ -53,9 +60,6 @@ struct PhotoModifications { QImage image, const PhotoModifications &mods); -void ApplyShapeMask( - QImage &image, - EditorData::CropType type, - RoundedCornersLevel cornersLevel); +void ApplyShapeMask(QImage &image, const PhotoModifications &mods); } // namespace Editor diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index 26693dcead..8a4a2ca931 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -1142,6 +1142,7 @@ void TopBar::setupUserpicButton( : (user && !user->isSelf() && !_peer->isBot()) ? &tr::lng_profile_set_personal_sure : nullptr; + const auto useForumShape = _peer->isForum() && !_peer->isBot(); return Editor::EditorData{ .about = (phrase ? (*phrase)( @@ -1153,7 +1154,9 @@ void TopBar::setupUserpicButton( .confirm = ((type == ChosenType::Suggest) ? tr::lng_profile_suggest_button(tr::now) : tr::lng_profile_set_photo_button(tr::now)), - .cropType = Editor::EditorData::CropType::Ellipse, + .cropType = (useForumShape + ? Editor::EditorData::CropType::RoundedRect + : Editor::EditorData::CropType::Ellipse), .keepAspectRatio = true, }; }; From ffacf3a582ae4359eb7a20e6da8d8fa4f3c6a564 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 28 Apr 2026 09:57:37 +0300 Subject: [PATCH 041/154] Collapsed deleted admin log message groups. --- Telegram/Resources/langs/lang.strings | 6 + .../admin_log/history_admin_log_inner.cpp | 463 ++++++++++++++++-- .../admin_log/history_admin_log_inner.h | 33 +- .../admin_log/history_admin_log_section.h | 21 + .../history/history_item_components.cpp | 3 +- 5 files changed, 488 insertions(+), 38 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6cca5b3ede..9363db85fa 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6501,6 +6501,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_edited_message" = "{from} edited message:"; "lng_admin_log_previous_message" = "Original message"; "lng_admin_log_deleted_message" = "{from} deleted message:"; +"lng_admin_log_deleted_messages_collapsed#one" = "{from} deleted {count} message from {names} ({link})."; +"lng_admin_log_deleted_messages_collapsed#other" = "{from} deleted {count} messages from {names} ({link})."; +"lng_admin_log_show_all" = "Show all"; +"lng_admin_log_hide_all" = "Hide all"; +"lng_admin_log_expand_more#one" = "Show {count} More Message"; +"lng_admin_log_expand_more#other" = "Show {count} More Messages"; "lng_admin_log_sent_message" = "{from} sent this message:"; "lng_admin_log_participant_joined" = "{from} joined the group"; "lng_admin_log_participant_joined_channel" = "{from} joined the channel"; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index eab916fe79..5486d29b43 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -11,7 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_web_page.h" #include "history/history_item.h" +#include "history/history_item_helpers.h" #include "history/history_item_components.h" +#include "history/history_item_reply_markup.h" #include "history/history_item_text.h" #include "history/admin_log/history_admin_log_section.h" #include "history/admin_log/history_admin_log_filter.h" @@ -92,14 +94,14 @@ void InnerWidget::enumerateItems(Method method) { constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom); // No displayed messages in this history. - if (_items.empty()) { + if (_displayItems.empty()) { return; } if (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) { return; } - auto begin = std::rbegin(_items), end = std::rend(_items); + auto begin = std::rbegin(_displayItems), end = std::rend(_displayItems); auto from = TopToBottom ? std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) { return this->itemTop(elem) + elem->height() <= top; }) : std::upper_bound(begin, end, _visibleBottom, [this](int bottom, auto &elem) { @@ -110,13 +112,13 @@ void InnerWidget::enumerateItems(Method method) { --from; } if (TopToBottom) { - Assert(itemTop(from->get()) + from->get()->height() > _visibleTop); + Assert(itemTop(*from) + (*from)->height() > _visibleTop); } else { - Assert(itemTop(from->get()) < _visibleBottom); + Assert(itemTop(*from) < _visibleBottom); } while (true) { - auto item = from->get(); + auto item = *from; auto itemtop = itemTop(item); auto itembottom = itemtop + item->height(); @@ -386,7 +388,7 @@ void InnerWidget::updateVisibleTopItem() { if (_visibleBottom == height()) { _visibleTopItem = nullptr; } else { - auto begin = std::rbegin(_items), end = std::rend(_items); + auto begin = std::rbegin(_displayItems), end = std::rend(_displayItems); auto from = std::lower_bound(begin, end, _visibleTop, [this](auto &&elem, int top) { return this->itemTop(elem) + elem->height() <= top; }); @@ -798,7 +800,11 @@ void InnerWidget::saveState(not_null memento) { memento->setAdmins(std::move(_admins)); memento->setAdminsCanEdit(std::move(_adminsCanEdit)); memento->setSearchQuery(std::move(_searchQuery)); + memento->setExpandedGroups(std::move(_expandedGroups)); if (!_filterChanged) { + clearExpandButtons(); + _displayItems.clear(); + _summaryItems.clear(); for (auto &item : _items) { item.clearView(); } @@ -807,6 +813,9 @@ void InnerWidget::saveState(not_null memento) { base::take(_eventIds), _upLoaded, _downLoaded); + memento->setDeleteEventMeta( + base::take(_itemEventIds), + base::take(_eventAdminIds)); base::take(_itemsByData); } _upLoaded = _downLoaded = true; // Don't load or handle anything anymore. @@ -819,7 +828,6 @@ void InnerWidget::restoreState(not_null memento) { auto items = memento->takeItems(); for (auto &item : items) { item.refreshView(this); - _itemsByData.emplace(item->data(), item.get()); } _items = std::move(items); @@ -828,11 +836,15 @@ void InnerWidget::restoreState(not_null memento) { _adminsCanEdit = memento->takeAdminsCanEdit(); _filter = memento->takeFilter(); _searchQuery = memento->takeSearchQuery(); + _expandedGroups = memento->takeExpandedGroups(); + _itemEventIds = memento->takeItemEventIds(); + _eventAdminIds = memento->takeEventAdminIds(); _upLoaded = memento->upLoaded(); _downLoaded = memento->downLoaded(); _filterChanged = false; updateMinMaxIds(); - updateSize(); + computeDeleteGroups(); + rebuildDisplayItems(); } void InnerWidget::preloadMore(Direction direction) { @@ -943,6 +955,13 @@ void InnerWidget::addEvents(Direction direction, const QVectordata(), item.get()); + _itemEventIds.emplace(item->data(), id); if (realId) { if (rememberRealMsgId) { _antiSpamValidator.addEventMsgId( @@ -991,7 +1011,8 @@ void InnerWidget::addEvents(Direction direction, const QVector= 0); - auto checkFrom = (direction == Direction::Up) - ? (_items.size() - addedCount) - : 1; // Should be ": 0", but zero is skipped anyway. - auto checkTo = (direction == Direction::Up) ? (_items.size() + 1) : (addedCount + 1); - for (auto i = checkFrom; i != checkTo; ++i) { - if (i > 0) { - const auto view = _items[i - 1].get(); - if (i < _items.size()) { - const auto previous = _items[i].get(); - view->setDisplayDate(view->dateTime().date() != previous->dateTime().date()); - const auto attach = view->computeIsAttachToPrevious(previous); - view->setAttachToPrevious(attach, previous); - previous->setAttachToNext(attach, view); - } else { - view->setDisplayDate(true); + +void InnerWidget::computeDeleteGroups() { + _deleteGroups.clear(); + + if (_items.empty()) { + return; + } + + // Walk _items and find consecutive runs of delete-action items + // from the same admin. Each delete event produces exactly 2 items + // (content + service). Delete events are identified via _eventAdminIds. + auto groupStart = -1; + auto groupAdmin = UserId(); + auto groupEventId = uint64(0); + auto groupEventCount = 0; + auto currentEventId = uint64(0); + + const auto finalizeGroup = [&](int endIndex) { + if (groupEventCount > 0) { + Assert(endIndex - groupStart >= groupEventCount * 2); + _deleteGroups.push_back({ + .eventId = groupEventId, + .adminId = groupAdmin, + .startIndex = groupStart, + .endIndex = endIndex, + .eventCount = groupEventCount, + }); + } + groupStart = -1; + groupAdmin = UserId(); + groupEventId = 0; + groupEventCount = 0; + }; + + for (auto i = 0, count = int(_items.size()); i < count; ++i) { + const auto item = _items[i]->data(); + const auto eit = _itemEventIds.find(item); + if (eit == _itemEventIds.end()) { + finalizeGroup(i); + continue; + } + const auto eventId = eit->second; + const auto adminIt = _eventAdminIds.find(eventId); + if (adminIt == _eventAdminIds.end()) { + finalizeGroup(i); + continue; + } + const auto adminId = adminIt->second; + + if (eventId != currentEventId) { + // New event encountered. + if (groupStart < 0 || adminId != groupAdmin) { + finalizeGroup(i); + groupStart = i; + groupAdmin = adminId; + } + currentEventId = eventId; + groupEventId = eventId; + ++groupEventCount; + } + // Items within the same event just extend the range. + } + finalizeGroup(int(_items.size())); +} + +OwnedItem InnerWidget::createGroupSummaryItem( + const DeleteGroup &group, + bool expanded) { + const auto admin = _history->owner().user(group.adminId); + const auto fromLink = admin->createOpenLink(); + const auto fromLinkText = tr::link(admin->name(), QString()); + + // Collect unique author names from content messages in the group. + constexpr auto kMaxNames = 4; + auto authorNames = QStringList(); + auto seenAuthors = base::flat_set(); + auto totalAuthors = 0; + for (auto i = group.startIndex; i < group.endIndex; ++i) { + const auto item = _items[i]->data(); + if (!item->isService()) { + const auto authorId = item->from()->id; + if (!seenAuthors.contains(authorId)) { + seenAuthors.emplace(authorId); + ++totalAuthors; + if (authorNames.size() < kMaxNames) { + authorNames.push_back(item->from()->name()); + } } } } + auto names = authorNames.join(u", "_q); + if (totalAuthors > kMaxNames) { + names += u", \u2026"_q; + } + + const auto toggleText = expanded + ? tr::lng_admin_log_hide_all(tr::now) + : tr::lng_admin_log_show_all(tr::now); + + const auto groupEventId = group.eventId; + const auto toggleLink = std::make_shared( + [weak = QPointer(this), groupEventId] { + if (const auto strong = weak.data()) { + strong->toggleDeleteGroup(groupEventId); + } + }); + + auto text = tr::lng_admin_log_deleted_messages_collapsed( + tr::now, + lt_count, + group.eventCount, + lt_from, + fromLinkText, + lt_names, + { names }, + lt_link, + tr::link(toggleText, QString()), + tr::marked); + + auto message = PreparedServiceText{ text }; + message.links.push_back(fromLink); + message.links.push_back(toggleLink); + + const auto date = (group.endIndex > group.startIndex) + ? _items[group.endIndex - 1]->dateTime().toSecsSinceEpoch() + : 0; + + return OwnedItem(this, _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = peerFromUser(group.adminId), + .date = TimeId(date), + }, std::move(message))); +} + +void InnerWidget::setupExpandButton( + not_null item, + int hiddenCount, + uint64 groupEventId) { + const auto text = tr::lng_admin_log_expand_more( + tr::now, + lt_count, + hiddenCount); + + auto markup = HistoryMessageMarkupData(); + markup.flags = ReplyMarkupFlag::Inline; + markup.rows.push_back({ + HistoryMessageMarkupButton( + HistoryMessageMarkupButton::Type::Callback, + text, + {}, + QByteArray()), + }); + item->updateReplyMarkup(std::move(markup)); + _expandMarkupItems.emplace(item); +} + +void InnerWidget::clearExpandButtons() { + for (const auto &item : _expandMarkupItems) { + item->updateReplyMarkup({}); + } + _expandMarkupItems.clear(); +} + +void InnerWidget::toggleDeleteGroup(uint64 groupEventId) { + _toggleAnimation.stop(); + + const auto scrollBefore = _visibleTop; + + // Find a stable scroll anchor from _items (not a summary item) + // near the current visible top position. + Element *anchor = nullptr; + auto anchorDelta = 0; + if (!_displayItems.empty()) { + auto begin = std::rbegin(_displayItems); + auto end = std::rend(_displayItems); + auto from = std::lower_bound(begin, end, _visibleTop, + [this](auto &elem, int top) { + return this->itemTop(elem) + elem->height() <= top; + }); + for (auto it = from; it != end; ++it) { + const auto view = *it; + if (_itemEventIds.contains(view->data())) { + anchor = view; + anchorDelta = _visibleTop - itemTop(view); + break; + } + } + } + + // Prepare a fallback anchor: the last item of the toggled group + // (always visible in both collapsed and expanded states). + Element *fallback = nullptr; + for (const auto &group : _deleteGroups) { + if (group.eventId == groupEventId && group.endIndex > 0) { + fallback = _items[group.endIndex - 1].get(); + break; + } + } + + if (_expandedGroups.contains(groupEventId)) { + _expandedGroups.erase(groupEventId); + } else { + _expandedGroups.insert(groupEventId); + } + + // Clear pointers that may reference summary items + // which will be destroyed by rebuildDisplayItems(). + _visibleTopItem = nullptr; + _scrollDateLastItem = nullptr; + _mouseActionItem = nullptr; + _selectedItem = nullptr; + Element::Hovered(nullptr); + Element::Pressed(nullptr); + Element::HoveredLink(nullptr); + Element::PressedLink(nullptr); + Element::Moused(nullptr); + + // Rebuild without triggering scroll restore inside updateSize(). + _skipScrollRestore = true; + rebuildDisplayItems(); + _skipScrollRestore = false; + + // Compute target scroll position. + auto scrollTarget = scrollBefore; + if (anchor && _itemsByData.contains(anchor->data())) { + scrollTarget = itemTop(anchor) + anchorDelta; + } else if (fallback && _itemsByData.contains(fallback->data())) { + scrollTarget = itemTop(fallback); + } + + // Snap to old position and animate to target. + _scrollToSignal.fire_copy(scrollBefore); + if (scrollBefore != scrollTarget) { + const auto from = scrollBefore; + const auto to = scrollTarget; + _toggleAnimation.start( + [=] { _scrollToSignal.fire_copy(anim::interpolate( + from, to, _toggleAnimation.value(1.))); }, + 0., + 1., + st::slideDuration, + anim::easeOutCubic); + } +} + +void InnerWidget::clearTransientDisplayPointers() { + const auto transient = [this](const Element *view) { + return view + && view->delegate().get() == this + && !_itemEventIds.contains(view->data()); + }; + if (transient(_visibleTopItem)) { + _visibleTopItem = nullptr; + _visibleTopFromItem = 0; + } + if (transient(_scrollDateLastItem)) { + _scrollDateLastItem = nullptr; + _scrollDateLastItemTop = 0; + } + if (transient(_mouseActionItem)) { + _mouseActionItem = nullptr; + _mouseAction = MouseAction::None; + } + if (transient(_selectedItem)) { + _selectedItem = nullptr; + _selectedText = TextSelection(); + } + if (transient(Element::Hovered())) { + Element::Hovered(nullptr); + ClickHandler::clearActive(); + } + if (transient(Element::Pressed())) { + Element::Pressed(nullptr); + ClickHandler::unpressed(); + } + if (transient(Element::HoveredLink())) { + Element::HoveredLink(nullptr); + ClickHandler::clearActive(); + } + if (transient(Element::PressedLink())) { + Element::PressedLink(nullptr); + ClickHandler::unpressed(); + } + if (transient(Element::Moused())) { + Element::Moused(nullptr); + } +} + +void InnerWidget::rebuildDisplayItems() { + clearTransientDisplayPointers(); + clearExpandButtons(); + _summaryItems.clear(); + _displayItems.clear(); + _itemsByData.clear(); + + const auto groupDisplayEnabled = _searchQuery.isEmpty(); + + // Build a set of group start indices for quick lookup. + auto groupByStart = base::flat_map(); // startIndex -> group index + for (auto g = 0, gc = int(_deleteGroups.size()); g < gc; ++g) { + groupByStart.emplace(_deleteGroups[g].startIndex, g); + } + + auto i = 0; + const auto count = int(_items.size()); + while (i < count) { + const auto git = groupByStart.find(i); + if (groupDisplayEnabled && git != groupByStart.end()) { + const auto &group = _deleteGroups[git->second]; + if (group.eventCount > 3) { + const auto expanded = _expandedGroups.contains(group.eventId); + if (expanded) { + for (auto j = group.startIndex; j < group.endIndex; ++j) { + const auto view = _items[j].get(); + _displayItems.push_back(view); + _itemsByData.emplace(view->data(), view); + } + } else if (group.endIndex >= group.startIndex + 2) { + // Collapsed: show only the content message + // (skip service header at endIndex-1). + const auto contentItem = _items[group.endIndex - 2]->data(); + setupExpandButton( + contentItem, + group.eventCount - 1, + group.eventId); + const auto contentView = _items[group.endIndex - 2].get(); + _displayItems.push_back(contentView); + _itemsByData.emplace(contentView->data(), contentView); + } + // Add summary item. + auto summary = createGroupSummaryItem(group, expanded); + const auto summaryView = summary.get(); + _displayItems.push_back(summaryView); + _itemsByData.emplace(summaryView->data(), summaryView); + _summaryItems.push_back(std::move(summary)); + + i = group.endIndex; + continue; + } + } + const auto view = _items[i].get(); + _displayItems.push_back(view); + _itemsByData.emplace(view->data(), view); + ++i; + } + + for (const auto view : _displayItems) { + view->setAttachToPrevious(false); + view->setAttachToNext(false); + } + for (auto d = 0, dc = int(_displayItems.size()); d < dc; ++d) { + const auto view = _displayItems[d]; + if (d + 1 < dc) { + const auto previous = _displayItems[d + 1]; + view->setDisplayDate( + view->dateTime().date() != previous->dateTime().date()); + const auto attach = view->computeIsAttachToPrevious(previous); + view->setAttachToPrevious(attach, previous); + previous->setAttachToNext(attach, view); + } else { + view->setDisplayDate(true); + } + } + updateSize(); } @@ -1043,12 +1409,13 @@ int InnerWidget::resizeGetHeight(int newWidth) { const auto resizeAllItems = (_itemsWidth != newWidth); auto newHeight = 0; - for (const auto &item : ranges::views::reverse(_items)) { - item->setY(newHeight); - if (item->pendingResize() || resizeAllItems) { - newHeight += item->resizeGetHeight(newWidth); + for (auto it = _displayItems.rbegin(); it != _displayItems.rend(); ++it) { + const auto view = *it; + view->setY(newHeight); + if (view->pendingResize() || resizeAllItems) { + newHeight += view->resizeGetHeight(newWidth); } else { - newHeight += item->height(); + newHeight += view->height(); } } _itemsWidth = newWidth; @@ -1058,6 +1425,9 @@ int InnerWidget::resizeGetHeight(int newWidth) { } void InnerWidget::restoreScrollPosition() { + if (_skipScrollRestore) { + return; + } const auto newVisibleTop = _visibleTopItem ? (itemTop(_visibleTopItem) + _visibleTopFromItem) : ScrollMax; @@ -1096,7 +1466,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { width(), std::min(st::msgMaxWidth / 2, width() / 2)); - auto begin = std::rbegin(_items), end = std::rend(_items); + auto begin = std::rbegin(_displayItems), end = std::rend(_displayItems); auto from = std::lower_bound(begin, end, clip.top(), [this](auto &elem, int top) { return this->itemTop(elem) + elem->height() <= top; }); @@ -1104,11 +1474,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { return this->itemTop(elem) < bottom; }); if (from != end) { - auto top = itemTop(from->get()); + auto top = itemTop(*from); context.translate(0, -top); p.translate(0, top); for (auto i = from; i != to; ++i) { - const auto view = i->get(); + const auto view = *i; context.outbg = view->hasOutLayout(); context.selection = (view == _selectedItem) ? _selectedText @@ -1199,6 +1569,13 @@ void InnerWidget::clearAfterFilterChange() { _selectedItem = nullptr; _selectedText = TextSelection(); _filterChanged = false; + clearExpandButtons(); + _displayItems.clear(); + _summaryItems.clear(); + _deleteGroups.clear(); + _expandedGroups.clear(); + _itemEventIds.clear(); + _eventAdminIds.clear(); _items.clear(); _eventIds.clear(); _itemsByData.clear(); @@ -1882,6 +2259,20 @@ void InnerWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton but _wasSelectedText = false; if (activated) { + // Intercept inline keyboard button clicks on items + // with our expand button markup. + if (_mouseActionItem) { + const auto item = _mouseActionItem->data(); + if (dynamic_cast(activated.get()) + && _expandMarkupItems.contains(item)) { + const auto it = _itemEventIds.find(item); + if (it != _itemEventIds.end()) { + mouseActionCancel(); + toggleDeleteGroup(it->second); + return; + } + } + } mouseActionCancel(); ActivateClickHandler(window(), activated, { button, @@ -1925,13 +2316,13 @@ void InnerWidget::updateSelected() { std::clamp(mousePosition.y(), _visibleTop, _visibleBottom)); auto itemPoint = QPoint(); - auto begin = std::rbegin(_items), end = std::rend(_items); + auto begin = std::rbegin(_displayItems), end = std::rend(_displayItems); auto from = (point.y() >= _itemsTop && point.y() < _itemsTop + _itemsHeight) ? std::lower_bound(begin, end, point.y(), [this](auto &elem, int top) { return this->itemTop(elem) + elem->height() <= top; }) : end; - const auto view = (from != end) ? from->get() : nullptr; + const auto view = (from != end) ? *from : nullptr; const auto item = view ? view->data().get() : nullptr; if (item) { Element::Moused(view); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index 7912104838..3dfece0e3e 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -48,6 +48,14 @@ namespace AdminLog { class SectionMemento; +struct DeleteGroup { + uint64 eventId = 0; + UserId adminId; + int startIndex = -1; + int endIndex = -1; + int eventCount = 0; +}; + class InnerWidget final : public Ui::RpWidget , public Ui::AbstractTooltipShower @@ -238,7 +246,6 @@ private: void checkPreloadMore(); void updateVisibleTopItem(); void preloadMore(Direction direction); - void itemsAdded(Direction direction, int addedCount); void updateSize(); void updateMinMaxIds(); void updateEmptyText(); @@ -248,6 +255,18 @@ private: void addEvents( Direction direction, const QVector &events); + void computeDeleteGroups(); + void rebuildDisplayItems(); + void clearTransientDisplayPointers(); + void toggleDeleteGroup(uint64 groupEventId); + OwnedItem createGroupSummaryItem( + const DeleteGroup &group, + bool expanded); + void setupExpandButton( + not_null item, + int hiddenCount, + uint64 groupEventId); + void clearExpandButtons(); [[nodiscard]] Element *viewForItem(const HistoryItem *item); [[nodiscard]] bool myView( not_null view) const; @@ -307,6 +326,18 @@ private: base::flat_map, Ui::PeerUserpicView> _userpics; base::flat_map, Ui::PeerUserpicView> _userpicsCache; base::flat_map _realIdsForReport; + + // Delete event grouping. + std::vector _displayItems; + std::vector _deleteGroups; + std::set _expandedGroups; + std::vector _summaryItems; + base::flat_map, uint64> _itemEventIds; + base::flat_map _eventAdminIds; + base::flat_set> _expandMarkupItems; + Ui::Animations::Simple _toggleAnimation; + bool _skipScrollRestore = false; + int _itemsTop = 0; int _itemsWidth = 0; int _itemsHeight = 0; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h index 3cff97fe47..99a1635300 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h @@ -159,6 +159,24 @@ public: QString takeSearchQuery() { return std::move(_searchQuery); } + void setExpandedGroups(std::set &&groups) { + _expandedGroups = std::move(groups); + } + std::set takeExpandedGroups() { + return std::move(_expandedGroups); + } + void setDeleteEventMeta( + base::flat_map, uint64> &&itemEventIds, + base::flat_map &&eventAdminIds) { + _itemEventIds = std::move(itemEventIds); + _eventAdminIds = std::move(eventAdminIds); + } + auto takeItemEventIds() { + return std::move(_itemEventIds); + } + auto takeEventAdminIds() { + return std::move(_eventAdminIds); + } private: not_null _channel; @@ -171,6 +189,9 @@ private: bool _downLoaded = true; FilterValue _filter; QString _searchQuery; + std::set _expandedGroups; + base::flat_map, uint64> _itemEventIds; + base::flat_map _eventAdminIds; }; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 45c1f4b7cb..47007d9c39 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -1060,7 +1060,8 @@ ClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const { if (rect.contains(point)) { if (_item->isAdminLogEntry() - && button.type != HistoryMessageMarkupButton::Type::Url) { + && button.type != HistoryMessageMarkupButton::Type::Url + && button.type != HistoryMessageMarkupButton::Type::Callback) { return ClickHandlerPtr(); } _savedCoords = point; From 3e3439bb6d23811099d698b7d1f6c3eea1fc976c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 28 Apr 2026 10:10:27 +0300 Subject: [PATCH 042/154] Fixed admin log display pointer cleanup. --- .../admin_log/history_admin_log_inner.cpp | 117 ++++++++++++------ .../admin_log/history_admin_log_inner.h | 10 +- 2 files changed, 86 insertions(+), 41 deletions(-) diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 5486d29b43..4fbd7d0d53 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -802,9 +802,7 @@ void InnerWidget::saveState(not_null memento) { memento->setSearchQuery(std::move(_searchQuery)); memento->setExpandedGroups(std::move(_expandedGroups)); if (!_filterChanged) { - clearExpandButtons(); - _displayItems.clear(); - _summaryItems.clear(); + clearDisplayItems(DisplayPointerScope::All); for (auto &item : _items) { item.clearView(); } @@ -1187,6 +1185,40 @@ void InnerWidget::setupExpandButton( } void InnerWidget::clearExpandButtons() { + const auto hasExpandButton = [&](const Element *view) { + if (!view) { + return false; + } + for (const auto &item : _items) { + if (item.get() == view) { + return _expandMarkupItems.contains(item->data()); + } + } + return false; + }; + if (hasExpandButton(_mouseActionItem)) { + _mouseActionItem = nullptr; + _mouseAction = MouseAction::None; + } + if (hasExpandButton(Element::Hovered())) { + Element::Hovered(nullptr); + ClickHandler::clearActive(); + } + if (hasExpandButton(Element::Pressed())) { + Element::Pressed(nullptr); + ClickHandler::unpressed(); + } + if (hasExpandButton(Element::HoveredLink())) { + Element::HoveredLink(nullptr); + ClickHandler::clearActive(); + } + if (hasExpandButton(Element::PressedLink())) { + Element::PressedLink(nullptr); + ClickHandler::unpressed(); + } + if (hasExpandButton(Element::Moused())) { + Element::Moused(nullptr); + } for (const auto &item : _expandMarkupItems) { item->updateReplyMarkup({}); } @@ -1235,17 +1267,7 @@ void InnerWidget::toggleDeleteGroup(uint64 groupEventId) { _expandedGroups.insert(groupEventId); } - // Clear pointers that may reference summary items - // which will be destroyed by rebuildDisplayItems(). - _visibleTopItem = nullptr; - _scrollDateLastItem = nullptr; - _mouseActionItem = nullptr; - _selectedItem = nullptr; - Element::Hovered(nullptr); - Element::Pressed(nullptr); - Element::HoveredLink(nullptr); - Element::PressedLink(nullptr); - Element::Moused(nullptr); + clearDisplayPointers(DisplayPointerScope::All); // Rebuild without triggering scroll restore inside updateSize(). _skipScrollRestore = true; @@ -1275,55 +1297,79 @@ void InnerWidget::toggleDeleteGroup(uint64 groupEventId) { } } -void InnerWidget::clearTransientDisplayPointers() { - const auto transient = [this](const Element *view) { - return view - && view->delegate().get() == this - && !_itemEventIds.contains(view->data()); +bool InnerWidget::displayPointerMatches( + const Element *view, + DisplayPointerScope pointerScope) const { + if (!view) { + return false; + } + for (const auto &item : _summaryItems) { + if (item.get() == view) { + return true; + } + } + if (pointerScope == DisplayPointerScope::All) { + for (const auto &item : _items) { + if (item.get() == view) { + return true; + } + } + } + return false; +} + +void InnerWidget::clearDisplayPointers(DisplayPointerScope pointerScope) { + const auto clearAll = (pointerScope == DisplayPointerScope::All); + const auto clearMember = [&](const Element *view) { + return clearAll || displayPointerMatches(view, pointerScope); }; - if (transient(_visibleTopItem)) { + if (clearMember(_visibleTopItem)) { _visibleTopItem = nullptr; _visibleTopFromItem = 0; } - if (transient(_scrollDateLastItem)) { + if (clearMember(_scrollDateLastItem)) { _scrollDateLastItem = nullptr; _scrollDateLastItemTop = 0; } - if (transient(_mouseActionItem)) { + if (clearMember(_mouseActionItem)) { _mouseActionItem = nullptr; _mouseAction = MouseAction::None; } - if (transient(_selectedItem)) { + if (clearMember(_selectedItem)) { _selectedItem = nullptr; _selectedText = TextSelection(); } - if (transient(Element::Hovered())) { + if (displayPointerMatches(Element::Hovered(), pointerScope)) { Element::Hovered(nullptr); ClickHandler::clearActive(); } - if (transient(Element::Pressed())) { + if (displayPointerMatches(Element::Pressed(), pointerScope)) { Element::Pressed(nullptr); ClickHandler::unpressed(); } - if (transient(Element::HoveredLink())) { + if (displayPointerMatches(Element::HoveredLink(), pointerScope)) { Element::HoveredLink(nullptr); ClickHandler::clearActive(); } - if (transient(Element::PressedLink())) { + if (displayPointerMatches(Element::PressedLink(), pointerScope)) { Element::PressedLink(nullptr); ClickHandler::unpressed(); } - if (transient(Element::Moused())) { + if (displayPointerMatches(Element::Moused(), pointerScope)) { Element::Moused(nullptr); } } -void InnerWidget::rebuildDisplayItems() { - clearTransientDisplayPointers(); +void InnerWidget::clearDisplayItems(DisplayPointerScope pointerScope) { clearExpandButtons(); + clearDisplayPointers(pointerScope); _summaryItems.clear(); _displayItems.clear(); _itemsByData.clear(); +} + +void InnerWidget::rebuildDisplayItems() { + clearDisplayItems(DisplayPointerScope::Transient); const auto groupDisplayEnabled = _searchQuery.isEmpty(); @@ -1561,17 +1607,8 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } void InnerWidget::clearAfterFilterChange() { - _visibleTopItem = nullptr; - _visibleTopFromItem = 0; - _scrollDateLastItem = nullptr; - _scrollDateLastItemTop = 0; - _mouseActionItem = nullptr; - _selectedItem = nullptr; - _selectedText = TextSelection(); _filterChanged = false; - clearExpandButtons(); - _displayItems.clear(); - _summaryItems.clear(); + clearDisplayItems(DisplayPointerScope::All); _deleteGroups.clear(); _expandedGroups.clear(); _itemEventIds.clear(); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index 3dfece0e3e..3fc990bea4 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -255,9 +255,17 @@ private: void addEvents( Direction direction, const QVector &events); + enum class DisplayPointerScope { + Transient, + All, + }; void computeDeleteGroups(); void rebuildDisplayItems(); - void clearTransientDisplayPointers(); + void clearDisplayItems(DisplayPointerScope pointerScope); + void clearDisplayPointers(DisplayPointerScope pointerScope); + [[nodiscard]] bool displayPointerMatches( + const Element *view, + DisplayPointerScope pointerScope) const; void toggleDeleteGroup(uint64 groupEventId); OwnedItem createGroupSummaryItem( const DeleteGroup &group, From 9b9fb348a0603ea1e6c9d2faf01298108c088824 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 28 Apr 2026 13:01:46 +0300 Subject: [PATCH 043/154] Fixed losing pasted text on cancel of send files box without caption. --- Telegram/SourceFiles/boxes/send_files_box.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 362e48efce..4e4c230d41 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -2256,7 +2256,7 @@ void SendFilesBox::requestToTakeTextWithTags() { return; } const auto text = _caption->getTextWithTags(); - if (!_prefilledCaptionText.text.isEmpty() && text.text.isEmpty()) { + if (text.text.isEmpty()) { return; } _textTaken = true; From 6aa406901cb26b49430baaf8fb6f0b0b759d68bf Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Apr 2026 18:14:27 +0700 Subject: [PATCH 044/154] Fix quote in topic messages replies. Fixes #30621. --- .../SourceFiles/history/view/history_view_list_widget.cpp | 4 ++-- Telegram/SourceFiles/history/view/history_view_list_widget.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index fd89ecd1c7..9f40a25ec3 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2752,9 +2752,9 @@ SelectedItems ListWidget::getSelectedItems() const { return collectSelectedItems(); } -const TextSelection &ListWidget::getSelectedTextRange( +TextSelection ListWidget::getSelectedTextRange( not_null item) const { - return _selectedTextRange; + return (_selectedTextItem == item) ? _selectedTextRange : TextSelection(); } int ListWidget::findItemIndexByY(int y) const { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index a3dcdb83c3..675378ce69 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -333,7 +333,7 @@ public: [[nodiscard]] TextForMimeData getSelectedText() const; [[nodiscard]] MessageIdsList getSelectedIds() const; [[nodiscard]] SelectedItems getSelectedItems() const; - [[nodiscard]] const TextSelection &getSelectedTextRange( + [[nodiscard]] TextSelection getSelectedTextRange( not_null item) const; void cancelSelection(); void selectItem(not_null item); From 2cfdf1b32ee4160da4d0816f6b9ff26bdf41cd81 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Apr 2026 20:50:59 +0700 Subject: [PATCH 045/154] Rename send as a file with extension. --- .../SourceFiles/boxes/edit_caption_box.cpp | 187 +++++++++++------- Telegram/SourceFiles/boxes/edit_caption_box.h | 4 + Telegram/SourceFiles/boxes/send_files_box.cpp | 113 ++++++++--- Telegram/SourceFiles/boxes/send_files_box.h | 6 + .../attach_abstract_single_file_preview.cpp | 65 ++++++ .../attach_abstract_single_file_preview.h | 13 ++ 6 files changed, 290 insertions(+), 98 deletions(-) diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 78d47ba6c2..e5b740e259 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "boxes/premium_limits_box.h" #include "boxes/premium_preview_box.h" +#include "boxes/send_files_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/field_autocomplete.h" #include "chat_helpers/message_field.h" @@ -545,6 +546,24 @@ void EditCaptionBox::rebuildPreview() { _content->heightValue( ) | rpl::start_to_stream(_contentHeight, _content->lifetime()); + if (const auto file = dynamic_cast( + _content.get())) { + file->setRenameEnabled(!_preparedList.files.empty()); + file->renameRequests( + ) | rpl::on_next([=] { + renameCurrentFile(); + }, _content->lifetime()); + } + + base::install_event_filter(_content.get(), [=](not_null e) { + if (e->type() == QEvent::ContextMenu) { + const auto mouse = static_cast(e.get()); + showMenu(mouse->globalPos(), false); + return base::EventFilterResult::Cancel; + } + return base::EventFilterResult::Continue; + }, _content->lifetime()); + _scroll->setOwnedWidget( object_ptr::fromRaw(_content.get())); @@ -728,81 +747,109 @@ void EditCaptionBox::setupControls() { } void EditCaptionBox::setupEditEventHandler() { - const auto menu - = lifetime().make_state>(); _editMediaClicks.events( ) | rpl::on_next([=] { - *menu = base::make_unique_q( - this, - st::popupMenuWithIcons); - (*menu)->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); - if (_isAllowedEditMedia) { - (*menu)->addAction(tr::lng_attach_replace(tr::now), [=] { - ChooseReplacement( - _controller, - _albumType, - crl::guard(this, [=](Ui::PreparedList &&list) { - setPreparedList(std::move(list)); - })); - }, &st::menuIconReplace); - } - using Type = Ui::PreparedFile::Type; - const auto canDraw = !_preparedList.files.empty() - ? (_preparedList.files.front().type == Type::Photo) - : (_isPhoto && !_asFile); - if (canDraw) { - (*menu)->addAction(tr::lng_context_draw(tr::now), [=] { - _photoEditorOpens.fire({}); - }, &st::menuIconDraw); - } - if (!_asFile && (_isPhoto || _isVideo)) { - if (hasSendLargePhotosOption()) { - const auto enabled = _sendLargePhotos; - Menu::AddCheckedAction( - menu->get(), - tr::lng_send_high_quality(tr::now), - [=] { - _sendLargePhotos = !enabled; - rebuildPreview(); - }, - &st::menuIconQualityHigh, - enabled); - } - if (_preparedList.hasSpoilerMenu(!_asFile)) { - const auto spoilered = hasSpoiler(); - Menu::AddCheckedAction( - menu->get(), - tr::lng_context_spoiler_effect(tr::now), - [=] { - _mediaEditManager.apply({ .type = spoilered - ? SendMenu::ActionType::SpoilerOff - : SendMenu::ActionType::SpoilerOn - }); - rebuildPreview(); - }, - &st::menuIconSpoiler, - spoilered); - } - if (_isVideo && !_preparedList.files.empty()) { - (*menu)->addAction(tr::lng_context_edit_cover(tr::now), [=] { - setupEditCoverHandler(); - }, &st::menuIconEdit); - if (_preparedList.files.front().videoCover != nullptr) { - (*menu)->addAction( - tr::lng_context_clear_cover(tr::now), - [=] { setupClearCoverHandler(); }, - &st::menuIconCancel); - } - } - } - if ((*menu)->empty()) { - *menu = nullptr; - } else { - (*menu)->popup(QCursor::pos()); - } + showMenu(QCursor::pos(), true); }, lifetime()); } +void EditCaptionBox::showMenu(QPoint globalPos, bool forceTopRight) { + _previewMenu = base::make_unique_q( + this, + st::popupMenuWithIcons); + if (forceTopRight) { + _previewMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + } + if (_isAllowedEditMedia) { + _previewMenu->addAction(tr::lng_attach_replace(tr::now), [=] { + ChooseReplacement( + _controller, + _albumType, + crl::guard(this, [=](Ui::PreparedList &&list) { + setPreparedList(std::move(list)); + })); + }, &st::menuIconReplace); + } + if (dynamic_cast(_content.get()) + && !_preparedList.files.empty()) { + _previewMenu->addAction(tr::lng_rename_file(tr::now), [=] { + renameCurrentFile(); + }, &st::menuIconEdit); + } + using Type = Ui::PreparedFile::Type; + const auto canDraw = !_preparedList.files.empty() + ? (_preparedList.files.front().type == Type::Photo) + : (_isPhoto && !_asFile); + if (canDraw) { + _previewMenu->addAction(tr::lng_context_draw(tr::now), [=] { + _photoEditorOpens.fire({}); + }, &st::menuIconDraw); + } + if (!_asFile && (_isPhoto || _isVideo)) { + if (hasSendLargePhotosOption()) { + const auto enabled = _sendLargePhotos; + Menu::AddCheckedAction( + _previewMenu.get(), + tr::lng_send_high_quality(tr::now), + [=] { + _sendLargePhotos = !enabled; + rebuildPreview(); + }, + &st::menuIconQualityHigh, + enabled); + } + if (_preparedList.hasSpoilerMenu(!_asFile)) { + const auto spoilered = hasSpoiler(); + Menu::AddCheckedAction( + _previewMenu.get(), + tr::lng_context_spoiler_effect(tr::now), + [=] { + _mediaEditManager.apply({ .type = spoilered + ? SendMenu::ActionType::SpoilerOff + : SendMenu::ActionType::SpoilerOn + }); + rebuildPreview(); + }, + &st::menuIconSpoiler, + spoilered); + } + if (_isVideo && !_preparedList.files.empty()) { + _previewMenu->addAction(tr::lng_context_edit_cover(tr::now), [=] { + setupEditCoverHandler(); + }, &st::menuIconEdit); + if (_preparedList.files.front().videoCover != nullptr) { + _previewMenu->addAction( + tr::lng_context_clear_cover(tr::now), + [=] { setupClearCoverHandler(); }, + &st::menuIconCancel); + } + } + } + if (_previewMenu->empty()) { + _previewMenu = nullptr; + } else { + _previewMenu->popup(globalPos); + } +} + +void EditCaptionBox::renameCurrentFile() { + if (_preparedList.files.empty()) { + return; + } + const auto &file = _preparedList.files.front(); + const auto allowExtensionEdit = file.path.isEmpty(); + _controller->show(Box(RenameFileBox, file.displayName, allowExtensionEdit, [=]( + QString displayName) { + _preparedList.files.front().displayName = displayName; + if (const auto filePreview = dynamic_cast( + _content.get())) { + filePreview->setDisplayName(displayName); + } else { + rebuildPreview(); + } + })); +} + void EditCaptionBox::setupPhotoEditorEventHandler() { const auto openedOnce = lifetime().make_state(false); _photoEditorOpens.events( diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index f08621f894..7e11b5b2fe 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -32,6 +32,7 @@ namespace Ui { class AbstractSinglePreview; class InputField; class EmojiButton; +class PopupMenu; class VerticalLayout; enum class AlbumType; } // namespace Ui @@ -90,6 +91,8 @@ protected: private: void rebuildPreview(); void setupEditEventHandler(); + void showMenu(QPoint globalPos, bool forceTopRight); + void renameCurrentFile(); void setupPhotoEditorEventHandler(); void setupEditCoverHandler(); void setupClearCoverHandler(); @@ -137,6 +140,7 @@ private: std::unique_ptr _autocomplete; base::unique_qptr _content; + base::unique_qptr _previewMenu; base::unique_qptr _emojiPanel; base::unique_qptr _emojiFilter; diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 4e4c230d41..78b30651fa 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -113,9 +113,12 @@ void FileDialogCallback( callback(std::move(*list)); } +} // namespace + void RenameFileBox( not_null box, const QString ¤tName, + bool allowExtensionEdit, Fn apply) { box->setTitle(tr::lng_rename_file()); const auto field = box->addRow(object_ptr( @@ -123,19 +126,25 @@ void RenameFileBox( st::settingsDeviceName, rpl::single(QString()), currentName)); - const auto extension = [&] { - if (currentName.isEmpty()) { - return u".png"_q; - } - const auto dot = currentName.lastIndexOf('.'); - return (dot >= 0) ? currentName.mid(dot) : QString(); - }(); - const auto nameWithoutExt = extension.isEmpty() - ? currentName - : currentName.left(currentName.size() - extension.size()); - const auto maxNameLength = kMaxDisplayNameLength - extension.size(); - field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0); - field->setText(nameWithoutExt); + QString extension; + if (allowExtensionEdit) { + field->setMaxLength(kMaxDisplayNameLength); + field->setText(currentName); + } else { + extension = [&] { + if (currentName.isEmpty()) { + return u".png"_q; + } + const auto dot = currentName.lastIndexOf('.'); + return (dot >= 0) ? currentName.mid(dot) : QString(); + }(); + const auto nameWithoutExt = extension.isEmpty() + ? currentName + : currentName.left(currentName.size() - extension.size()); + const auto maxNameLength = kMaxDisplayNameLength - extension.size(); + field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0); + field->setText(nameWithoutExt); + } field->selectAll(); box->setFocusCallback([=] { field->setFocusFast(); @@ -146,12 +155,18 @@ void RenameFileBox( field->showError(); return; } - if ((newName.size() + extension.size()) > kMaxDisplayNameLength) { + if (allowExtensionEdit) { + if (newName.size() > kMaxDisplayNameLength) { + field->showError(); + return; + } + } else if ((newName.size() + extension.size()) + > kMaxDisplayNameLength) { field->showError(); return; } const auto weak = base::make_weak(box); - apply(newName + extension); + apply(allowExtensionEdit ? newName : (newName + extension)); if (const auto strong = weak.get()) { strong->closeBox(); } @@ -165,6 +180,8 @@ void RenameFileBox( }); } +namespace { + void EditFileCaptionBox( not_null box, const style::ComposeControls &st, @@ -416,11 +433,13 @@ SendFilesBox::Block::Block( media->setCanShowHighQualityBadge(first.canUseHighQualityPhoto()); _preview.reset(media); } else { - _preview.reset(Ui::CreateChild( + const auto single = Ui::CreateChild( parent.get(), st, first, - captionContext)); + captionContext); + single->setRenameEnabled(!SkipCaption(first, way)); + _preview.reset(single); } } _preview->show(); @@ -492,6 +511,19 @@ rpl::producer SendFilesBox::Block::itemModifyRequest() const { } } +rpl::producer SendFilesBox::Block::itemRenameRequest() const { + using namespace rpl::mappers; + + const auto preview = _preview.get(); + const auto from = _from; + if (_isAlbum || _isSingleMedia) { + return rpl::never(); + } else { + const auto single = static_cast(preview); + return single->renameRequests() | rpl::map_to(from); + } +} + rpl::producer<> SendFilesBox::Block::orderUpdated() const { if (_isAlbum) { const auto album = static_cast(_preview.get()); @@ -506,6 +538,10 @@ void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) { const auto media = static_cast( _preview.get()); media->setSendWay(way); + } else { + const auto single = static_cast( + _preview.get()); + single->setRenameEnabled(!SkipCaption((*_items)[_from], way)); } return; } @@ -1467,6 +1503,32 @@ void SendFilesBox::pushBlock(int from, int till) { entry.videoCover = nullptr; }); }; + const auto renameFile = [=](int fileIndex) { + if (fileIndex < 0 || fileIndex >= _list.files.size()) { + return; + } + const auto &file = _list.files[fileIndex]; + const auto canEditFileData = !SkipCaption( + file, + _sendWay.current()); + if (!canEditFileData) { + return; + } + const auto allowExtensionEdit = file.path.isEmpty(); + _show->show(Box( + RenameFileBox, + file.displayName, + allowExtensionEdit, + [=](QString newName) { + const auto displayName = std::move(newName); + _list.files[fileIndex].displayName = displayName; + if (!setDisplayNameInSingleFilePreview( + fileIndex, + displayName)) { + refreshAllAfterChanges(from); + } + })); + }; const auto showContextMenu = [=]( int fileIndex, QPoint globalPosition, @@ -1501,17 +1563,7 @@ void SendFilesBox::pushBlock(int from, int till) { _sendWay.current()); if (canEditFileData) { state->menu->addAction(tr::lng_rename_file(tr::now), [=] { - auto &file = _list.files[fileIndex]; - _show->show(Box(RenameFileBox, file.displayName, [=]( - QString newName) { - const auto displayName = std::move(newName); - _list.files[fileIndex].displayName = displayName; - if (!setDisplayNameInSingleFilePreview( - fileIndex, - displayName)) { - refreshAllAfterChanges(from); - } - })); + renameFile(fileIndex); }, &st::menuIconEdit); state->menu->addAction( tr::lng_context_upload_edit_caption(tr::now), @@ -1626,6 +1678,11 @@ void SendFilesBox::pushBlock(int from, int till) { openInPhotoEditor(index); }, widget->lifetime()); + block.itemRenameRequest( + ) | rpl::on_next([=](int index) { + renameFile(index); + }, widget->lifetime()); + block.orderUpdated() | rpl::on_next([=]{ if (_priceTag) { _priceTagBg = QImage(); diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 90f8a143a9..f752bb1c7b 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -93,6 +93,11 @@ using SendFilesCheck = Fn show, not_null peer); +void RenameFileBox( + not_null box, + const QString ¤tName, + bool allowExtensionEdit, + Fn apply); using SendFilesConfirmed = Fn, @@ -178,6 +183,7 @@ private: [[nodiscard]] rpl::producer itemDeleteRequest() const; [[nodiscard]] rpl::producer itemReplaceRequest() const; [[nodiscard]] rpl::producer itemModifyRequest() const; + [[nodiscard]] rpl::producer itemRenameRequest() const; [[nodiscard]] rpl::producer<> orderUpdated() const; void setSendWay(Ui::SendFilesWay way); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp index 5cdf08fc80..76ea640353 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp @@ -56,6 +56,7 @@ AbstractSingleFilePreview::AbstractSingleFilePreview( _deleteMedia->hide(); _editMedia->hide(); } + setMouseTracking(true); } AbstractSingleFilePreview::~AbstractSingleFilePreview() = default; @@ -66,6 +67,10 @@ rpl::producer<> AbstractSingleFilePreview::editRequests() const { }) | rpl::flatten_latest(); } +rpl::producer<> AbstractSingleFilePreview::renameRequests() const { + return _renameRequests.events(); +} + rpl::producer<> AbstractSingleFilePreview::deleteRequests() const { return _deleteMedia->clicks() | rpl::to_empty; } @@ -74,6 +79,17 @@ rpl::producer<> AbstractSingleFilePreview::modifyRequests() const { return rpl::never<>(); } +void AbstractSingleFilePreview::setRenameEnabled(bool enabled) { + if (_renameEnabled == enabled) { + return; + } + _renameEnabled = enabled; + if (!_renameEnabled) { + _namePressed = false; + applyCursor(style::cur_default); + } +} + void AbstractSingleFilePreview::setDisplayName(const QString &displayName) { _data.name = displayName; updateTextWidthFor(_data); @@ -279,4 +295,53 @@ void AbstractSingleFilePreview::setData(Data data) { updateDataGeometry(); } +void AbstractSingleFilePreview::mousePressEvent(QMouseEvent *e) { + if (isOverName(e->pos())) { + _namePressed = true; + } +} + +void AbstractSingleFilePreview::mouseMoveEvent(QMouseEvent *e) { + applyCursor(isOverName(e->pos()) + ? style::cur_pointer + : style::cur_default); +} + +void AbstractSingleFilePreview::mouseReleaseEvent(QMouseEvent *e) { + if (base::take(_namePressed) + && (e->button() == Qt::LeftButton) + && isOverName(e->pos())) { + _renameRequests.fire({}); + } +} + +QRect AbstractSingleFilePreview::nameRect() const { + const auto w = width() + - st::boxPhotoPadding.left() + - st::boxPhotoPadding.right(); + const auto &st = !isThumbedLayout(_data) + ? st::attachPreviewLayout + : st::attachPreviewThumbLayout; + const auto nameleft = st.thumbSize + st.thumbSkip; + const auto nametop = st.nameTop; + const auto x = (width() - w) / 2, y = 0; + return style::rtlrect( + x + nameleft, + y + nametop, + _data.nameWidth, + st::semiboldFont->height, + width()); +} + +bool AbstractSingleFilePreview::isOverName(QPoint point) const { + return _renameEnabled && nameRect().contains(point); +} + +void AbstractSingleFilePreview::applyCursor(style::cursor cursor) { + if (_cursor != cursor) { + _cursor = cursor; + setCursor(_cursor); + } +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h index bfe05fde32..11b7356f0f 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h @@ -30,7 +30,9 @@ public: [[nodiscard]] rpl::producer<> deleteRequests() const override; [[nodiscard]] rpl::producer<> editRequests() const override; + [[nodiscard]] rpl::producer<> renameRequests() const; [[nodiscard]] rpl::producer<> modifyRequests() const override; + void setRenameEnabled(bool enabled); virtual void setDisplayName(const QString &displayName); virtual void setCaption(const TextWithTags &caption); @@ -58,10 +60,16 @@ protected: private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; void updateTextWidthFor(Data &data); void updateDataGeometry(); [[nodiscard]] QRect captionRect() const; + [[nodiscard]] QRect nameRect() const; + [[nodiscard]] bool isOverName(QPoint point) const; + void applyCursor(style::cursor cursor); const style::ComposeControls &_st; const AttachControls::Type _type; @@ -71,6 +79,11 @@ private: object_ptr _editMedia = { nullptr }; object_ptr _deleteMedia = { nullptr }; + rpl::event_stream<> _renameRequests; + + style::cursor _cursor = style::cur_default; + bool _namePressed = false; + bool _renameEnabled = false; }; From f396a5499c5bb31def998d8256baad3c67b981de Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 28 Apr 2026 19:27:23 +0300 Subject: [PATCH 046/154] Fixed multiline reply layout for poll and story messages. --- Telegram/SourceFiles/boxes/polls.style | 6 ++-- Telegram/SourceFiles/dialogs/dialogs.style | 5 +++ .../history/view/history_view_reply.cpp | 35 ++++--------------- .../history/view/history_view_reply.h | 2 -- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index bcdafbe095..960a49a5b8 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -48,8 +48,10 @@ historyPollRadio: Radio(defaultRadio) { rippleAreaPadding: 8px; } historyPollCheckboxRadius: 3px; -historyPollReplyIcon: icon {{ "dialogs/dialogs_chatlist_poll-17x17", windowFg }}; -historyPollReplyIconSkip: 3px; +historyPollReplyIcon: IconEmoji { + icon: icon {{ "dialogs/dialogs_chatlist_poll-17x17", windowFg }}; + padding: margins(0px, 0px, 3px, 0px); +} historyPollRadioOpacity: 0.7; historyPollRadioOpacityOver: 1.; historyPollDuration: 300; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 78f560bd29..2878c7fc38 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -577,6 +577,11 @@ dialogsMiniReplyStory: DialogsMiniIcon { skipMedia: 5px; } +historyReplyStoryIcon: IconEmoji { + icon: icon {{ "mini_reply_story", dialogsTextFg, point(0px, 1px) }}; + padding: margins(0px, 0px, 4px, 0px); +} + dialogsUnreadMention: ThreeStateIcon { icon: icon{{ "dialogs/dialogs_chatlist_mention-18x18", dialogsMentionIconFg }}; over: icon{{ "dialogs/dialogs_chatlist_mention-18x18", dialogsMentionIconFg }}; diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index 02d79e12a3..6ddb53ad9d 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -390,8 +390,6 @@ void Reply::update( _hasPreview = hasPreview ? 1 : 0; _displaying = data->displaying() ? 1 : 0; _multiline = data->multiline() ? 1 : 0; - _replyToStory = (fields.storyId != 0); - _replyToPoll = (messagePoll && !pollAnswer) ? 1 : 0; const auto hasQuoteIcon = _displaying && fields.manualQuote && !fields.quote.empty(); @@ -420,13 +418,17 @@ void Reply::update( .margin = QMargins(0, st::lineWidth, st::lineWidth, 0), })).append(pollAnswer->text) : messagePoll - ? TextWithEntities().append(messagePoll->question) + ? Ui::Text::Colorized( + Ui::Text::IconEmoji(&st::historyPollReplyIcon) + ).append(messagePoll->question) : (message && (fields.quote.empty() || !fields.manualQuote)) ? message->inReplyText() : !fields.quote.empty() ? fields.quote : story - ? story->inReplyText() + ? Ui::Text::Colorized( + Ui::Text::IconEmoji(&st::historyReplyStoryIcon) + ).append(story->inReplyText()) : externalMedia ? externalMedia->toPreview({ .hideSender = true, @@ -675,15 +677,10 @@ void Reply::updateName( + (_hasQuoteIcon ? st::messageTextStyle.blockquote.icon.width() : 0); - const auto storySkip = fields.storyId - ? (st::dialogsMiniReplyStory.skipText - + st::dialogsMiniReplyStory.icon.icon.width()) - : 0; const auto optimalTextSize = _multiline ? countMultilineOptimalSize(previewSkip) : QSize( (previewSkip - + storySkip + std::min(_text.maxWidth(), st::maxSignatureSize)), st::normalFont->height); _maxWidth = std::max(nameMaxWidth, optimalTextSize.width()); @@ -1016,26 +1013,6 @@ void Reply::paint( copy->linkFg = owned->color(); replyToTextPalette = &*copy; } - if (_replyToStory) { - st::dialogsMiniReplyStory.icon.icon.paint( - p, - textLeft + firstLineSkip, - textTop, - w + 2 * x, - replyToTextPalette->linkFg->c); - firstLineSkip += st::dialogsMiniReplyStory.skipText - + st::dialogsMiniReplyStory.icon.icon.width(); - } - if (_replyToPoll) { - st::historyPollReplyIcon.paint( - p, - textLeft + firstLineSkip, - textTop, - w + 2 * x, - replyToTextPalette->linkFg->c); - firstLineSkip += st::historyPollReplyIconSkip - + st::historyPollReplyIcon.width(); - } _text.draw(p, { .position = { textLeft, textTop }, .geometry = textGeometry(textw, firstLineSkip), diff --git a/Telegram/SourceFiles/history/view/history_view_reply.h b/Telegram/SourceFiles/history/view/history_view_reply.h index d622125267..00b553f5fc 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.h +++ b/Telegram/SourceFiles/history/view/history_view_reply.h @@ -152,8 +152,6 @@ private: mutable int _nameVersion = 0; uint8 _hiddenSenderColorIndexPlusOne : 7 = 0; uint8 _hasQuoteIcon : 1 = 0; - uint8 _replyToStory : 1 = 0; - uint8 _replyToPoll : 1 = 0; uint8 _expanded : 1 = 0; mutable uint8 _expandable : 1 = 0; mutable uint8 _minHeightExpandable : 1 = 0; From 8f51e0e1075ec0e3defcc007e6e61336e25b2813 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 29 Apr 2026 18:47:03 +0300 Subject: [PATCH 047/154] Saved selected photos with their original timestamps. --- .../menu/menu_item_download_files.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/menu/menu_item_download_files.cpp b/Telegram/SourceFiles/menu/menu_item_download_files.cpp index a105e58f1e..b363365ec6 100644 --- a/Telegram/SourceFiles/menu/menu_item_download_files.cpp +++ b/Telegram/SourceFiles/menu/menu_item_download_files.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_item_download_files.h" #include "base/base_file_utilities.h" +#include "base/unixtime.h" #include "core/application.h" #include "core/core_settings.h" #include "core/file_utilities.h" @@ -113,10 +114,16 @@ void AddAction( }; auto views = std::vector>(); + auto dates = std::vector(); for (const auto &[photo, fullId] : photos) { if (const auto view = photo->createMediaView()) { view->wanted(Data::PhotoSize::Large, fullId); views.push_back(view); + const auto photoDate = photo->date(); + const auto item = session->data().message(fullId); + dates.push_back(photoDate + ? photoDate + : (item ? item->date() : TimeId(0))); } } @@ -139,7 +146,18 @@ void AddAction( auto lastPath = QString(); for (auto i = 0; i < views.size(); i++) { lastPath = fullPath(i + 1); - views[i]->saveToFile(lastPath); + if (views[i]->saveToFile(lastPath) && dates[i] > 0) { + auto f = QFile(lastPath); + if (f.open(QIODevice::ReadWrite)) { + const auto when = base::unixtime::parse(dates[i]); + f.setFileTime( + when, + QFileDevice::FileModificationTime); + f.setFileTime( + when, + QFileDevice::FileAccessTime); + } + } } if (showToast) { showToast(lastPath); From 94eb4ebbae108f383024b4520d4f2350e819e1d1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 29 Apr 2026 20:39:46 +0300 Subject: [PATCH 048/154] Fixed sticker set menu thumbnails for non-square stickers. --- .../SourceFiles/api/api_stickers_creator.cpp | 2 +- .../SourceFiles/ui/dynamic_thumbnails.cpp | 87 +++++++++++++------ Telegram/SourceFiles/ui/dynamic_thumbnails.h | 3 + 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/Telegram/SourceFiles/api/api_stickers_creator.cpp b/Telegram/SourceFiles/api/api_stickers_creator.cpp index 5b1b85d371..f7a037a35a 100644 --- a/Telegram/SourceFiles/api/api_stickers_creator.cpp +++ b/Telegram/SourceFiles/api/api_stickers_creator.cpp @@ -153,7 +153,7 @@ void FillChooseOwnedSetMenu( const auto identifier = set->identifier(); const auto coverDocument = set->lookupThumbnailDocument(); auto thumbnail = coverDocument - ? Ui::MakeDocumentThumbnail( + ? Ui::MakeDocumentThumbnailFit( coverDocument, Data::FileOriginStickerSet(set->id, set->accessHash)) : nullptr; diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp index b8c6af32ab..8530f8c93f 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp @@ -32,6 +32,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace { +enum class MediaThumbnailMode { + Crop, + CenterCrop, + Fit, +}; + class PeerUserpic final : public DynamicImage { public: PeerUserpic(not_null peer, bool forceRound); @@ -70,7 +76,7 @@ public: explicit MediaThumbnail( Data::FileOrigin origin, bool forceRound, - bool centerCrop); + MediaThumbnailMode mode); QImage image(int size) override; void subscribeToUpdates(Fn callback) override; @@ -83,7 +89,7 @@ protected: [[nodiscard]] Data::FileOrigin origin() const; [[nodiscard]] bool forceRound() const; - [[nodiscard]] bool centerCrop() const; + [[nodiscard]] MediaThumbnailMode mode() const; [[nodiscard]] virtual Main::Session &session() = 0; [[nodiscard]] virtual Thumb loaded(Data::FileOrigin origin) = 0; @@ -92,7 +98,7 @@ protected: private: const Data::FileOrigin _origin; const bool _forceRound; - const bool _centerCrop; + const MediaThumbnailMode _mode; QImage _full; rpl::lifetime _subscription; QImage _prepared; @@ -106,7 +112,7 @@ public: not_null photo, Data::FileOrigin origin, bool forceRound, - bool centerCrop); + MediaThumbnailMode mode); std::shared_ptr clone() override; @@ -126,7 +132,7 @@ public: not_null video, Data::FileOrigin origin, bool forceRound, - bool centerCrop); + MediaThumbnailMode mode); std::shared_ptr clone() override; @@ -384,23 +390,44 @@ void PeerUserpic::processNewPhoto() { MediaThumbnail::MediaThumbnail( Data::FileOrigin origin, bool forceRound, - bool centerCrop) + MediaThumbnailMode mode) : _origin(origin) , _forceRound(forceRound) -, _centerCrop(centerCrop) { +, _mode(mode) { } QImage MediaThumbnail::image(int size) { const auto ratio = style::DevicePixelRatio(); if (_prepared.width() != size * ratio) { + const auto full = QSize(size, size) * ratio; if (_full.isNull()) { _prepared = QImage( - QSize(size, size) * ratio, + full, QImage::Format_ARGB32_Premultiplied); - _prepared.fill(Qt::black); + _prepared.setDevicePixelRatio(ratio); + _prepared.fill(_mode == MediaThumbnailMode::Fit + ? Qt::transparent + : Qt::black); + } else if (_mode == MediaThumbnailMode::Fit) { + auto scaled = _full.scaled( + full, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + const auto scaledSize = QSizeF(scaled.size()) / ratio; + scaled.setDevicePixelRatio(ratio); + _prepared = QImage(full, QImage::Format_ARGB32_Premultiplied); + _prepared.setDevicePixelRatio(ratio); + _prepared.fill(Qt::transparent); + + auto p = QPainter(&_prepared); + p.drawImage( + QPointF( + (size - scaledSize.width()) / 2., + (size - scaledSize.height()) / 2.), + scaled); } else { auto source = QRect(); - if (_centerCrop) { + if (_mode == MediaThumbnailMode::CenterCrop) { const auto side = std::min(_full.width(), _full.height()); const auto x = (_full.width() - side) / 2; const auto y = (_full.height() - side) / 2; @@ -461,16 +488,16 @@ bool MediaThumbnail::forceRound() const { return _forceRound; } -bool MediaThumbnail::centerCrop() const { - return _centerCrop; +MediaThumbnailMode MediaThumbnail::mode() const { + return _mode; } PhotoThumbnail::PhotoThumbnail( not_null photo, Data::FileOrigin origin, bool forceRound, - bool centerCrop) -: MediaThumbnail(origin, forceRound, centerCrop) + MediaThumbnailMode mode) +: MediaThumbnail(origin, forceRound, mode) , _photo(photo) { } @@ -479,7 +506,7 @@ std::shared_ptr PhotoThumbnail::clone() { _photo, origin(), forceRound(), - centerCrop()); + mode()); } Main::Session &PhotoThumbnail::session() { @@ -505,8 +532,8 @@ VideoThumbnail::VideoThumbnail( not_null video, Data::FileOrigin origin, bool forceRound, - bool centerCrop) -: MediaThumbnail(origin, forceRound, centerCrop) + MediaThumbnailMode mode) +: MediaThumbnail(origin, forceRound, mode) , _video(video) { } @@ -515,7 +542,7 @@ std::shared_ptr VideoThumbnail::clone() { _video, origin(), forceRound(), - centerCrop()); + mode()); } Main::Session &VideoThumbnail::session() { @@ -1063,13 +1090,13 @@ std::shared_ptr MakeStoryThumbnail( photo, id, true, - false); + MediaThumbnailMode::Crop); }, [&](not_null video) -> Result { return std::make_shared( video, id, true, - false); + MediaThumbnailMode::Crop); }); } @@ -1098,7 +1125,7 @@ std::shared_ptr MakePhotoThumbnail( photo, fullId, false, - false); + MediaThumbnailMode::Crop); } std::shared_ptr MakePhotoThumbnailCenterCrop( @@ -1108,7 +1135,7 @@ std::shared_ptr MakePhotoThumbnailCenterCrop( photo, fullId, false, - true); + MediaThumbnailMode::CenterCrop); } std::shared_ptr MakeDocumentThumbnail( @@ -1118,7 +1145,7 @@ std::shared_ptr MakeDocumentThumbnail( document, fullId, false, - false); + MediaThumbnailMode::Crop); } std::shared_ptr MakeDocumentThumbnail( @@ -1128,7 +1155,17 @@ std::shared_ptr MakeDocumentThumbnail( document, origin, false, - false); + MediaThumbnailMode::Crop); +} + +std::shared_ptr MakeDocumentThumbnailFit( + not_null document, + Data::FileOrigin origin) { + return std::make_shared( + document, + origin, + false, + MediaThumbnailMode::Fit); } std::shared_ptr MakeDocumentThumbnailCenterCrop( @@ -1138,7 +1175,7 @@ std::shared_ptr MakeDocumentThumbnailCenterCrop( document, fullId, false, - true); + MediaThumbnailMode::CenterCrop); } std::shared_ptr MakeDocumentFilePreviewThumbnail( diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.h b/Telegram/SourceFiles/ui/dynamic_thumbnails.h index 7a16058935..5ebff97a9a 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.h +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.h @@ -54,6 +54,9 @@ class DynamicImage; [[nodiscard]] std::shared_ptr MakeDocumentThumbnail( not_null document, Data::FileOrigin origin); +[[nodiscard]] std::shared_ptr MakeDocumentThumbnailFit( + not_null document, + Data::FileOrigin origin); [[nodiscard]] std::shared_ptr MakeDocumentThumbnailCenterCrop( not_null document, FullMsgId fullId); From be37bed7bda0c3779a2019dda6e89439b6a1345f Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 27 Apr 2026 12:13:16 +0700 Subject: [PATCH 049/154] [ai] Add rule about u""_q literal. --- AGENTS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ec1650302b..78db1ff125 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,6 +156,18 @@ QString currentTitle = tr::lng_settings_title(tr::now); rpl::producer nameProducer = GetNameProducer(); ``` +**Use `_q` for QString literals:** + +Prefer the project literal `u"..."_q` instead of the verbose `QStringLiteral("...")` macro when creating `QString` values: + +```cpp +// Prefer this: +auto text = u"Settings"_q; + +// Instead of this: +auto text = QStringLiteral("Settings"); +``` + ## API Usage ### API Schema Files From 0d9c808f0451fb5f7db1afdc9cdd9d9d94f81610 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Apr 2026 16:21:24 +0700 Subject: [PATCH 050/154] Copy / paste formatting to / from HTML. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 787445cdc6..26ea73e65c 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 787445cdc68e06d370ca81af2f42028abc7f503b +Subproject commit 26ea73e65cadcce411c6370183d807131f3836bc From 0628b23226569a6c2b5dfd1d2be27514210893b9 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 29 Apr 2026 23:02:13 +0700 Subject: [PATCH 051/154] Add two force-confirmation cases to hidden urls. --- .../SourceFiles/core/click_handler_types.cpp | 16 +++++++++++++++- .../inline_bots/bot_attach_web_view.cpp | 9 +++++---- .../inline_bots/bot_attach_web_view.h | 2 +- .../window/window_session_controller.cpp | 1 + 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 1d1f20c7f4..3d8ba2377a 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -74,6 +74,19 @@ constexpr auto kReminderSetToastDuration = 4 * crl::time(1000); return result; } +[[nodiscard]] bool IsTelegramShortLinkHost(const QUrl &url) { + using namespace qthelp; + + return regex_match( + "(^|\\.)(telegram\\.(me|dog)|t\\.me)$", + url.host(), + RegExOption::CaseInsensitive).valid(); +} + +[[nodiscard]] bool HiddenUrlRequiresConfirmation(const QUrl &url) { + return UrlRequiresConfirmation(url) || IsTelegramShortLinkHost(url); +} + // Possible context owners: media viewer, profile, history widget. void SearchByHashtag(ClickContext context, const QString &tag) { @@ -254,7 +267,8 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { const auto parsedUrl = url.startsWith(u"tonsite://"_q) ? QUrl(url) : QUrl::fromUserInput(url); - if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) { + if (HiddenUrlRequiresConfirmation(parsedUrl) + && !base::IsCtrlPressed()) { const auto my = context.value(); if (!my.show) { Core::App().hideMediaView(); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 4b99329104..41f9d6aaea 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -937,7 +937,7 @@ void WebViewInstance::resolve() { }, [&](WebViewSourceLinkBotProfile) { confirmOpen([=] { requestMain(); - }); + }, !_context.maySkipConfirmation); }, [&](WebViewSourceLinkAttachMenu data) { requestWithMenuAdd(); }, [&](WebViewSourceMainMenu) { @@ -1039,9 +1039,10 @@ void WebViewInstance::resolveApp( }).send(); } -void WebViewInstance::confirmOpen(Fn done) { - if (_bot->isVerified() - || _session->local().isPeerTrustedOpenWebView(_bot->id)) { +void WebViewInstance::confirmOpen(Fn done, bool forceConfirmation) { + if (!forceConfirmation + && (_bot->isVerified() + || _session->local().isPeerTrustedOpenWebView(_bot->id))) { done(); return; } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index f1c35dafd0..1304584c63 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -252,7 +252,7 @@ private: const QString &appname, const QString &startparam, ConfirmType confirmType); - void confirmOpen(Fn done); + void confirmOpen(Fn done, bool forceConfirmation = false); void confirmAppOpen( bool writeAccess, Fn done, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 9f1fa45258..2abe006229 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -798,6 +798,7 @@ void SessionNavigation::showPeerByLinkResolved( .context = { .controller = parentController(), .fullscreen = info.botAppFullScreen, + .maySkipConfirmation = !info.botAppForceConfirmation, }, .button = { .startCommand = startCommand }, .source = InlineBots::WebViewSourceLinkBotProfile{ From b7a20e1878912688c5b9168fe3cee423dcbee956 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 30 Apr 2026 16:53:35 +0700 Subject: [PATCH 052/154] Send sticker from set box to correct place. --- .../SourceFiles/history/history_widget.cpp | 8 +++++++ Telegram/SourceFiles/history/history_widget.h | 3 ++- .../history_view_compose_controls.cpp | 4 ++++ .../controls/history_view_compose_controls.h | 1 + .../view/history_view_chat_section.cpp | 5 ++++ .../history/view/history_view_chat_section.h | 3 ++- .../view/history_view_scheduled_section.cpp | 5 ++++ .../view/history_view_scheduled_section.h | 3 ++- .../SourceFiles/info/info_content_widget.cpp | 9 +++++++ .../SourceFiles/info/info_content_widget.h | 10 ++++++++ .../SourceFiles/info/info_layer_widget.cpp | 11 +++++++++ Telegram/SourceFiles/info/info_layer_widget.h | 1 + .../SourceFiles/info/info_wrap_widget.cpp | 11 +++++++++ Telegram/SourceFiles/info/info_wrap_widget.h | 2 ++ .../info/settings/info_settings_widget.cpp | 14 +++++++++++ .../info/settings/info_settings_widget.h | 2 ++ Telegram/SourceFiles/mainwidget.cpp | 10 +++++++- Telegram/SourceFiles/mainwidget.h | 5 ++++ .../business/settings_shortcut_messages.cpp | 15 ++++++++++++ .../SourceFiles/settings/settings_common.cpp | 4 ++++ .../SourceFiles/settings/settings_common.h | 17 +++++++++++++ .../SourceFiles/window/section_widget.cpp | 12 +++++++++- Telegram/SourceFiles/window/section_widget.h | 7 ++++++ .../window/window_session_controller.cpp | 24 ++++++++++++++++++- .../window/window_session_controller.h | 7 ++++++ 25 files changed, 187 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0f3a21055b..d3f51ae454 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2137,6 +2137,14 @@ void HistoryWidget::fileChosen(ChatHelpers::FileChosen &&data) { } } +bool HistoryWidget::processChosenSticker(ChatHelpers::FileChosen &&chosen) { + if (!_peer) { + return false; + } + fileChosen(std::move(chosen)); + return true; +} + void HistoryWidget::saveCloudDraft() { controller()->session().api().saveCurrentDraftToCloud(); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 3f79a66915..5730a121ce 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -295,7 +295,8 @@ public: void confirmDeleteSelected(); void clearSelected(); - [[nodiscard]] SendMenu::Details sendMenuDetails() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const override; + bool processChosenSticker(ChatHelpers::FileChosen &&chosen) override; [[nodiscard]] SendMenu::Details saveMenuDetails() const; bool sendExistingDocument( not_null document, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index e376e72623..413440d7da 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1879,6 +1879,10 @@ bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { return true; } +void ComposeControls::processChosenSticker(FileChosen &&chosen) { + _stickerOrEmojiChosen.fire(std::move(chosen)); +} + rpl::producer ComposeControls::fileChosen() const { return _fileChosen.events(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 2adc246881..cfd792d62c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -202,6 +202,7 @@ public: Fn, Api::SendOptions)> confirmed); + void processChosenSticker(FileChosen &&chosen); [[nodiscard]] rpl::producer fileChosen() const; [[nodiscard]] rpl::producer photoChosen() const; [[nodiscard]] rpl::producer jumpToItemRequests() const; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 138dd8f110..92f92f2a0c 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1822,6 +1822,11 @@ SendMenu::Details ChatWidget::sendMenuDetails() const { return SendMenu::Details{ .type = type }; } +bool ChatWidget::processChosenSticker(ChatHelpers::FileChosen &&chosen) { + _composeControls->processChosenSticker(std::move(chosen)); + return true; +} + FullReplyTo ChatWidget::replyTo() const { if (auto custom = _composeControls->replyingToMessage()) { const auto item = custom.messageId diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index 997e4f09d9..355659d39e 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -312,7 +312,8 @@ private: mtpRequestId *const saveEditMsgRequestId, bool spoilered); void chooseAttach(std::optional overrideSendImagesAsPhotos); - [[nodiscard]] SendMenu::Details sendMenuDetails() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const override; + bool processChosenSticker(ChatHelpers::FileChosen &&chosen) override; [[nodiscard]] FullReplyTo replyTo() const; [[nodiscard]] HistoryItem *lookupRepliesRoot() const; [[nodiscard]] Data::ForumTopic *lookupTopic(); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index cae8dc159e..1d0133f9d8 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -940,6 +940,11 @@ SendMenu::Details ScheduledWidget::sendMenuDetails() const { return { .type = type, .effectAllowed = effectAllowed }; } +bool ScheduledWidget::processChosenSticker(ChatHelpers::FileChosen &&chosen) { + _composeControls->processChosenSticker(std::move(chosen)); + return true; +} + void ScheduledWidget::cornerButtonsShowAtPosition( Data::MessagePosition position) { showAtPosition(position); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index 3d03c52a37..82ee98d3eb 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -239,7 +239,8 @@ private: bool spoilered); void highlightSingleNewMessage(const Data::MessagesSlice &slice); void chooseAttach(); - [[nodiscard]] SendMenu::Details sendMenuDetails() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const override; + bool processChosenSticker(ChatHelpers::FileChosen &&chosen) override; void pushReplyReturn(not_null item); void checkReplyReturns(); diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index de6dad657d..b41b18d85d 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "menu/menu_send.h" #include "ui/controls/swipe_handler.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/fields/input_field.h" @@ -426,6 +427,14 @@ void ContentWidget::saveChanges(FnMut done) { done(); } +SendMenu::Details ContentWidget::sendMenuDetails() const { + return {}; +} + +bool ContentWidget::processChosenSticker(ChatHelpers::FileChosen &&) { + return false; +} + void ContentWidget::refreshSearchField(bool shown) { auto search = _controller->searchFieldController(); if (search && shown) { diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 3b89c10c6a..4f064c969d 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -40,6 +40,14 @@ namespace Ui::Menu { struct MenuCallback; } // namespace Ui::Menu +namespace ChatHelpers { +struct FileChosen; +} // namespace ChatHelpers + +namespace SendMenu { +struct Details; +} // namespace SendMenu + namespace Info::Settings { struct Tag; } // namespace Info::Settings @@ -141,6 +149,8 @@ public: -> rpl::producer; virtual void saveChanges(FnMut done); + [[nodiscard]] virtual SendMenu::Details sendMenuDetails() const; + virtual bool processChosenSticker(ChatHelpers::FileChosen &&chosen); [[nodiscard]] int scrollBottomSkip() const; [[nodiscard]] rpl::producer scrollBottomSkipValue() const; diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index 845ab1b778..2c98bc987b 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -31,6 +31,7 @@ LayerWidget::LayerWidget( not_null memento) : _controller(controller) , _contentWrap(this, controller, Wrap::Layer, memento) { + controller->registerActiveLayerSection(_contentWrap.data()); setupHeightConsumers(); controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate()); } @@ -40,6 +41,7 @@ LayerWidget::LayerWidget( not_null memento) : _controller(controller) , _contentWrap(memento->takeContent(this, Wrap::Layer)) { + controller->registerActiveLayerSection(_contentWrap.data()); setupHeightConsumers(); controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate()); } @@ -161,6 +163,7 @@ void LayerWidget::parentResized() { if (parentWidth < MinimalSupportedWidth()) { Ui::FocusPersister persister(this); restoreFloatPlayerDelegate(); + unregisterActiveLayerSection(); auto memento = std::make_shared(std::move(_contentWrap)); @@ -374,11 +377,19 @@ void LayerWidget::restoreFloatPlayerDelegate() { } } +void LayerWidget::unregisterActiveLayerSection() { + if (_contentWrap) { + _controller->unregisterActiveLayerSection(_contentWrap.data()); + } +} + void LayerWidget::closeHook() { + unregisterActiveLayerSection(); restoreFloatPlayerDelegate(); } LayerWidget::~LayerWidget() { + unregisterActiveLayerSection(); if (!Core::Quitting()) { restoreFloatPlayerDelegate(); } diff --git a/Telegram/SourceFiles/info/info_layer_widget.h b/Telegram/SourceFiles/info/info_layer_widget.h index df8bc2726d..403c04271c 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.h +++ b/Telegram/SourceFiles/info/info_layer_widget.h @@ -70,6 +70,7 @@ private: void setupHeightConsumers(); void setContentHeight(int height); + void unregisterActiveLayerSection(); [[nodiscard]] QRect countGeometry(int newWidth); not_null _controller; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index b7c909927a..a2f15f1418 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "lang/lang_keys.h" #include "lang/lang_numbers_animation.h" +#include "menu/menu_send.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_info.h" #include "styles/style_profile.h" @@ -900,6 +901,16 @@ std::shared_ptr WrapWidget::createMemento() { return std::make_shared(std::move(stack)); } +SendMenu::Details WrapWidget::sendMenuDetails() const { + return _content ? _content->sendMenuDetails() : SendMenu::Details(); +} + +bool WrapWidget::processChosenSticker(ChatHelpers::FileChosen &&chosen) { + return _content + ? _content->processChosenSticker(std::move(chosen)) + : false; +} + rpl::producer WrapWidget::desiredHeightValue() const { return _desiredHeights.events_starting_with(desiredHeightForContent()) | rpl::flatten_latest(); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index fdfe5e8348..1924368169 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -119,6 +119,8 @@ public: bool showBackFromStackInternal(const Window::SectionShow ¶ms); void removeFromStack(const std::vector
§ions); std::shared_ptr createMemento() override; + [[nodiscard]] SendMenu::Details sendMenuDetails() const override; + bool processChosenSticker(ChatHelpers::FileChosen &&chosen) override; rpl::producer desiredHeightValue() const override; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 9764d2f269..0cb5aa3161 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/sections/settings_main.h" #include "settings/sections/settings_information.h" #include "settings/settings_common_session.h" +#include "menu/menu_send.h" #include "ui/ui_utility.h" namespace Info { @@ -187,6 +188,19 @@ void Widget::saveChanges(FnMut done) { _inner->sectionSaveChanges(std::move(done)); } +SendMenu::Details Widget::sendMenuDetails() const { + if (const auto provider + = dynamic_cast( + _inner.get())) { + return provider->sendMenuDetails(); + } + return ContentWidget::sendMenuDetails(); +} + +bool Widget::processChosenSticker(ChatHelpers::FileChosen &&chosen) { + return _inner->processChosenSticker(std::move(chosen)); +} + void Widget::showFinished() { _inner->showFinished(); diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index 5070a7f2a5..0acbf6acd1 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -80,6 +80,8 @@ public: not_null memento); void saveChanges(FnMut done) override; + [[nodiscard]] SendMenu::Details sendMenuDetails() const override; + bool processChosenSticker(ChatHelpers::FileChosen &&chosen) override; void showFinished() override; void setInnerFocus() override; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index beac8028dd..36135477ff 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1130,7 +1130,15 @@ void MainWidget::exportTopBarHeightUpdated() { } SendMenu::Details MainWidget::sendMenuDetails() const { - return _history->sendMenuDetails(); + return _mainSection + ? _mainSection->sendMenuDetails() + : _history->sendMenuDetails(); +} + +bool MainWidget::processChosenSticker(ChatHelpers::FileChosen &&chosen) { + return _mainSection + ? _mainSection->processChosenSticker(std::move(chosen)) + : _history->processChosenSticker(std::move(chosen)); } void MainWidget::dialogsCancelled() { diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 0a200c4a1d..996726c544 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -19,6 +19,10 @@ namespace Bot { struct SendCommandRequest; } // namespace Bot +namespace ChatHelpers { +struct FileChosen; +} // namespace ChatHelpers + namespace SendMenu { struct Details; } // namespace SendMenu @@ -144,6 +148,7 @@ public: void checkMainSectionToLayer(); [[nodiscard]] SendMenu::Details sendMenuDetails() const; + bool processChosenSticker(ChatHelpers::FileChosen &&chosen); [[nodiscard]] bool animatingShow() const; diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index bfb96335f6..5e1ba123b0 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -74,6 +74,7 @@ using namespace HistoryView; class ShortcutMessages : public AbstractSection + , public SendMenuDetailsProvider , private WindowListDelegate , private CornerButtonsDelegate { public: @@ -93,6 +94,8 @@ public: [[nodiscard]] rpl::producer title() override; [[nodiscard]] rpl::producer<> sectionShowBack() override; + [[nodiscard]] SendMenu::Details sendMenuDetails() const override; + bool processChosenSticker(ChatHelpers::FileChosen &&chosen) override; void setInnerFocus() override; rpl::producer selectedListValue() override; @@ -1447,6 +1450,18 @@ void ShortcutMessages::finishSending() { showAtEnd(); } +SendMenu::Details ShortcutMessages::sendMenuDetails() const { + return {}; +} + +bool ShortcutMessages::processChosenSticker(ChatHelpers::FileChosen &&chosen) { + if (!_composeControls) { + return false; + } + _composeControls->processChosenSticker(std::move(chosen)); + return true; +} + void ShortcutMessages::showAtEnd() { showAtPosition(Data::MaxMessagePosition); } diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index addab0de05..e1855d81b1 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -296,6 +296,10 @@ AbstractSection::AbstractSection( : _controller(controller) { } +bool AbstractSection::processChosenSticker(ChatHelpers::FileChosen &&) { + return false; +} + void AbstractSection::build( not_null container, SectionBuildMethod method) { diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index c55fbec289..9a62768575 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -24,6 +24,14 @@ struct SelectedItems; enum class SelectionAction; } // namespace Info +namespace ChatHelpers { +struct FileChosen; +} // namespace ChatHelpers + +namespace SendMenu { +struct Details; +} // namespace SendMenu + namespace Main { class Session; } // namespace Main @@ -126,6 +134,7 @@ public: virtual void sectionSaveChanges(FnMut done) { done(); } + virtual bool processChosenSticker(ChatHelpers::FileChosen &&chosen); virtual void showFinished() { _showFinished.fire({}); } @@ -195,6 +204,14 @@ private: }; +class SendMenuDetailsProvider { +public: + [[nodiscard]] virtual SendMenu::Details sendMenuDetails() const = 0; + + virtual ~SendMenuDetailsProvider() = default; + +}; + enum class IconType { Rounded, Round, diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index b417c4a2bb..dc63a402a1 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -24,8 +24,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "history/history.h" #include "history/history_item.h" -#include "settings/sections/settings_premium.h" +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "menu/menu_send.h" +#include "settings/sections/settings_premium.h" #include "window/section_memento.h" #include "window/window_slide_animation.h" #include "window/window_session_controller.h" @@ -241,6 +243,14 @@ Main::Session &AbstractSectionWidget::session() const { return _controller->session(); } +SendMenu::Details AbstractSectionWidget::sendMenuDetails() const { + return {}; +} + +bool AbstractSectionWidget::processChosenSticker(ChatHelpers::FileChosen &&) { + return false; +} + SectionWidget::SectionWidget( QWidget *parent, not_null controller, diff --git a/Telegram/SourceFiles/window/section_widget.h b/Telegram/SourceFiles/window/section_widget.h index 2d288810bf..97d23fa18d 100644 --- a/Telegram/SourceFiles/window/section_widget.h +++ b/Telegram/SourceFiles/window/section_widget.h @@ -17,9 +17,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PeerData; namespace ChatHelpers { +struct FileChosen; class Show; } // namespace ChatHelpers +namespace SendMenu { +struct Details; +} // namespace SendMenu + namespace Data { struct ReactionId; class ForumTopic; @@ -72,6 +77,8 @@ public: virtual bool returnTabbedSelector() { return false; } + [[nodiscard]] virtual SendMenu::Details sendMenuDetails() const; + virtual bool processChosenSticker(ChatHelpers::FileChosen &&chosen); private: const not_null _controller; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 2abe006229..5b3484e156 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_chat_switch_process.h" #include "window/window_controller.h" #include "window/window_filters_menu.h" +#include "window/section_widget.h" #include "window/window_separate_id.h" #include "info/channel_statistics/earn/info_channel_earn_list.h" #include "info/peer_gifts/info_peer_gifts_widget.h" @@ -297,6 +298,9 @@ SendMenu::Details MainWindowShow::sendMenuDetails() const { if (!window) { return SendMenu::Details(); } + if (const auto section = window->activeLayerSection()) { + return section->sendMenuDetails(); + } return window->content()->sendMenuDetails(); } @@ -318,7 +322,11 @@ void MainWindowShow::processChosenSticker( ChatHelpers::FileChosen &&chosen) const { if (const auto window = _window.get()) { Ui::PostponeCall(window, [=, chosen = std::move(chosen)]() mutable { - window->stickerOrEmojiChosen(std::move(chosen)); + if (const auto section = window->activeLayerSection()) { + section->processChosenSticker(std::move(chosen)); + } else { + window->content()->processChosenSticker(std::move(chosen)); + } }); } } @@ -3120,6 +3128,20 @@ bool SessionController::isLayerShown() const { return _window->isLayerShown(); } +void SessionController::registerActiveLayerSection(SectionWidget *section) { + _activeLayerSection = section; +} + +void SessionController::unregisterActiveLayerSection(SectionWidget *section) { + if (_activeLayerSection == section) { + _activeLayerSection = nullptr; + } +} + +SectionWidget *SessionController::activeLayerSection() const { + return _activeLayerSection.data(); +} + not_null SessionController::content() const { return widget()->sessionContent(); } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index bead50cbe1..9ff7406eb2 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_type.h" #include "window/window_adaptive.h" +#include + class PhotoData; class MainWidget; class MainWindow; @@ -108,6 +110,7 @@ using GifPauseReason = ChatHelpers::PauseReason; using GifPauseReasons = ChatHelpers::PauseReasons; class SectionMemento; +class SectionWidget; class Controller; class FiltersMenu; class ChatPreviewManager; @@ -552,6 +555,9 @@ public: } void removeLayerBlackout(); [[nodiscard]] bool isLayerShown() const; + void registerActiveLayerSection(SectionWidget *section); + void unregisterActiveLayerSection(SectionWidget *section); + [[nodiscard]] SectionWidget *activeLayerSection() const; struct ShowCalendarDescriptor { Dialogs::Key chat; @@ -843,6 +849,7 @@ private: rpl::variable _connectingBottomSkip; rpl::event_stream _stickerOrEmojiChosen; + QPointer _activeLayerSection; PeerData *_showEditPeer = nullptr; rpl::variable _openedFolder; From bb3924a125f98c0d6e5e7fe9e2f5824f2c9f0022 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 30 Apr 2026 17:49:05 +0700 Subject: [PATCH 053/154] Don't use isolated/only-custom emoji in AboutView. --- Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp index 1e321ae4e4..c336323dad 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp @@ -122,7 +122,8 @@ EmojiPack::EmojiPack(not_null session) EmojiPack::~EmojiPack() = default; bool EmojiPack::add(not_null view) { - if (view->data()->textAppearing()) { + if (view->data()->textAppearing() + || view->Get()) { return false; } else if (const auto custom = view->onlyCustomEmoji()) { _onlyCustomItems.emplace(view); From b1f80218476db6cf657cdd3db4e07d263a0dce77 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 30 Apr 2026 18:15:25 +0700 Subject: [PATCH 054/154] Update tgcalls submodule. --- Telegram/ThirdParty/tgcalls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls index 24876ebca7..8099768559 160000 --- a/Telegram/ThirdParty/tgcalls +++ b/Telegram/ThirdParty/tgcalls @@ -1 +1 @@ -Subproject commit 24876ebca7da10f92dc972225734337f9e793054 +Subproject commit 8099768559edb0efd2d1b300090c18141226e9a8 From 9eda44ca316bf06eb0bbe72dd607e85c960a7ca8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 30 Apr 2026 23:52:45 +0700 Subject: [PATCH 055/154] Improve checks for video frame sizes. --- Telegram/SourceFiles/data/data_types.cpp | 10 +- .../ffmpeg/ffmpeg_frame_generator.cpp | 17 +++- .../SourceFiles/ffmpeg/ffmpeg_utility.cpp | 96 +++++++++++++++---- Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h | 2 + .../history/view/media/history_view_gif.cpp | 13 ++- .../inline_bot_layout_internal.cpp | 27 ++++-- .../media/clip/media_clip_ffmpeg.cpp | 44 +++------ Telegram/SourceFiles/media/media_common.h | 11 +++ .../media/streaming/media_streaming_common.h | 2 + .../media/streaming/media_streaming_file.cpp | 1 + .../streaming/media_streaming_utility.cpp | 6 ++ .../streaming/media_streaming_video_track.cpp | 19 +++- .../SourceFiles/overview/overview_layout.cpp | 15 +-- 13 files changed, 184 insertions(+), 79 deletions(-) diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index 8fed3ecd30..ab60b8bfb6 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_types.h" +#include "media/media_common.h" #include "ui/widgets/fields/input_field.h" #include "storage/cache/storage_cache_types.h" #include "base/openssl_help.h" @@ -157,8 +158,9 @@ BusinessShortcutId BusinessShortcutIdFromMessage( bool GoodStickerDimensions(int width, int height) { // Show all .webp (except very large ones) as stickers, // allow to open them in media viewer to see details. - constexpr auto kLargetsStickerSide = 2560; - return (width > 0) - && (height > 0) - && (width * height <= kLargetsStickerSide * kLargetsStickerSide); + constexpr auto kLargestStickerSide = 2560; + return ::Media::ValidFrameSize( + width, + height, + kLargestStickerSide * kLargestStickerSide); } diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.cpp index 0f35c5ff23..13e3f7f865 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ffmpeg/ffmpeg_frame_generator.h" #include "ffmpeg/ffmpeg_utility.h" +#include "media/media_common.h" #include "base/debug_log.h" namespace FFmpeg { @@ -15,6 +16,8 @@ namespace { constexpr auto kMaxArea = 1920 * 1080 * 4; +using ::Media::ValidFrameSize; + } // namespace class FrameGenerator::Impl final { @@ -103,7 +106,11 @@ FrameGenerator::Impl::Impl(const QByteArray &bytes) const auto info = _format->streams[_streamId]; _rotation = ReadRotationFromMetadata(info); //_aspect = ValidateAspectRatio(info->sample_aspect_ratio); - _codec = MakeCodecPointer({ .stream = info }); + _codec = MakeCodecPointer({ + .stream = info, + .hwAllowed = false, + .videoMaxArea = kMaxArea, + }); } int FrameGenerator::Impl::Read(void *opaque, uint8_t *buf, int buf_size) { @@ -157,7 +164,7 @@ FrameGenerator::Frame FrameGenerator::Impl::renderCurrent( const auto width = frame->width; const auto height = frame->height; if (!width || !height) { - LOG(("Webm Error: Bad frame size: %1x%2 ").arg(width).arg(height)); + LOG(("Webm Error: Bad frame size %1x%2").arg(width).arg(height)); return {}; } @@ -167,6 +174,10 @@ FrameGenerator::Frame FrameGenerator::Impl::renderCurrent( } if (!GoodStorageForFrame(storage, size)) { storage = CreateFrameStorage(size); + if (storage.isNull()) { + LOG(("Webm Error: Bad frame size %1x%2").arg(width).arg(height)); + return {}; + } } const auto dx = (size.width() - scaled.width()) / 2; const auto dy = (size.height() - scaled.height()) / 2; @@ -317,7 +328,7 @@ void FrameGenerator::Impl::readNextFrame() { while (true) { auto result = avcodec_receive_frame(_codec.get(), frame.get()); if (result >= 0) { - if (frame->width * frame->height > kMaxArea) { + if (!ValidFrameSize(frame->width, frame->height, kMaxArea)) { return; } _next.frame = std::move(frame); diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 56e612358e..0c60c62204 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #endif // !Q_OS_WIN && !Q_OS_MAC #include +#include +#include #ifdef LIB_FFMPEG_USE_QT_PRIVATE_API #include @@ -44,6 +46,7 @@ constexpr auto kAlignImageBy = 64; constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied; constexpr auto kMaxScaleByAspectRatio = 16; constexpr auto kAvioBlockSize = 4096; +constexpr auto kMaxFrameStorageBytes = 64 * 1024 * 1024; constexpr auto kTimeUnknown = std::numeric_limits::min(); constexpr auto kDurationMax = crl::time(std::numeric_limits::max()); @@ -56,11 +59,47 @@ struct HwAccelDescriptor { AVPixelFormat format = AV_PIX_FMT_NONE; }; -void AlignedImageBufferCleanupHandler(void* data) { +struct AlignedFrameStorageLayout { + int width = 0; + int height = 0; + int bytesPerLine = 0; + int totalBytes = 0; +}; + +void AlignedImageBufferCleanupHandler(void *data) { const auto buffer = static_cast(data); delete[] buffer; } +[[nodiscard]] bool ComputeAlignedFrameStorageLayout( + QSize size, + AlignedFrameStorageLayout *out) { + const auto width = size.width(); + const auto height = size.height(); + if (width <= 0 || height <= 0) { + return false; + } + const auto widthAlign = kAlignImageBy / kPixelBytesSize; + const auto widthRemainder = width % widthAlign; + const auto widthPadding = widthRemainder + ? (widthAlign - widthRemainder) + : 0; + const auto alignedWidth = int64_t(width) + widthPadding; + const auto bytesPerLine = int64_t(alignedWidth) * kPixelBytesSize; + if (bytesPerLine > kMaxFrameStorageBytes) { + return false; + } + const auto totalBytes = int64_t(bytesPerLine) * height + kAlignImageBy; + if (totalBytes > kMaxFrameStorageBytes) { + return false; + } + out->width = width; + out->height = height; + out->bytesPerLine = int(bytesPerLine); + out->totalBytes = int(totalBytes); + return true; +} + [[nodiscard]] bool IsValidAspectRatio(AVRational aspect) { return (aspect.num > 0) && (aspect.den > 0) @@ -394,6 +433,10 @@ CodecPointer MakeCodecPointer(CodecDescriptor descriptor) { return {}; } context->pkt_timebase = stream->time_base; + if ((descriptor.videoMaxArea > 0) + && (context->codec_type == AVMEDIA_TYPE_VIDEO)) { + context->max_pixels = descriptor.videoMaxArea; + } av_opt_set(context, "threads", "auto", 0); av_opt_set_int(context, "refcounted_frames", 1, 0); @@ -684,27 +727,26 @@ bool GoodStorageForFrame(const QImage &storage, QSize size) { // Create a QImage of desired size where all the data is properly aligned. QImage CreateFrameStorage(QSize size) { - const auto width = size.width(); - const auto height = size.height(); - const auto widthAlign = kAlignImageBy / kPixelBytesSize; - const auto neededWidth = width + ((width % widthAlign) - ? (widthAlign - (width % widthAlign)) - : 0); - const auto perLine = neededWidth * kPixelBytesSize; - const auto buffer = new uchar[size_t(perLine) * height + kAlignImageBy]; - const auto cleanupData = static_cast(buffer); + auto layout = AlignedFrameStorageLayout(); + if (!ComputeAlignedFrameStorageLayout(size, &layout)) { + return {}; + } + const auto buffer = new (std::nothrow) uchar[layout.totalBytes]; + if (!buffer) { + return {}; + } const auto address = reinterpret_cast(buffer); const auto alignedBuffer = buffer + ((address % kAlignImageBy) ? (kAlignImageBy - (address % kAlignImageBy)) : 0); return QImage( alignedBuffer, - width, - height, - perLine, + layout.width, + layout.height, + layout.bytesPerLine, kImageFormat, AlignedImageBufferCleanupHandler, - cleanupData); + buffer); } void UnPremultiply(QImage &dst, const QImage &src) { @@ -712,21 +754,32 @@ void UnPremultiply(QImage &dst, const QImage &src) { // as an image in QImage::Format_ARGB32 format. if (!GoodStorageForFrame(dst, src.size())) { dst = CreateFrameStorage(src.size()); + if (dst.isNull()) { + return; + } } const auto srcPerLine = src.bytesPerLine(); const auto dstPerLine = dst.bytesPerLine(); const auto width = src.width(); const auto height = src.height(); + if (width <= 0 || height <= 0) { + return; + } + const auto packedLine = int64_t(width) * kPixelBytesSize; + const auto packedCount = int64_t(width) * height; + const auto fast = (srcPerLine == packedLine) + && (dstPerLine == packedLine) + && (packedCount <= kMaxFrameStorageBytes); auto srcBytes = src.bits(); auto dstBytes = dst.bits(); - if (srcPerLine != width * 4 || dstPerLine != width * 4) { + if (!fast) { for (auto i = 0; i != height; ++i) { UnPremultiplyLine(dstBytes, srcBytes, width); srcBytes += srcPerLine; dstBytes += dstPerLine; } } else { - UnPremultiplyLine(dstBytes, srcBytes, width * height); + UnPremultiplyLine(dstBytes, srcBytes, int(packedCount)); } } @@ -734,14 +787,21 @@ void PremultiplyInplace(QImage &image) { const auto perLine = image.bytesPerLine(); const auto width = image.width(); const auto height = image.height(); + if (width <= 0 || height <= 0) { + return; + } + const auto packedLine = int64_t(width) * kPixelBytesSize; + const auto packedCount = int64_t(width) * height; + const auto fast = (perLine == packedLine) + && (packedCount <= kMaxFrameStorageBytes); auto bytes = image.bits(); - if (perLine != width * 4) { + if (!fast) { for (auto i = 0; i != height; ++i) { PremultiplyLine(bytes, bytes, width); bytes += perLine; } } else { - PremultiplyLine(bytes, bytes, width * height); + PremultiplyLine(bytes, bytes, int(packedCount)); } } diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h index 99becdd061..f5f26b7da6 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/bytes.h" #include "base/algorithm.h" +#include #include #include @@ -153,6 +154,7 @@ using CodecPointer = std::unique_ptr; struct CodecDescriptor { not_null stream; bool hwAllowed = false; + int64_t videoMaxArea = 0; }; [[nodiscard]] CodecPointer MakeCodecPointer(CodecDescriptor descriptor); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 00ca1fb633..742e5f81ff 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session_settings.h" #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" +#include "media/media_common.h" #include "media/player/media_player_instance.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" @@ -72,6 +73,8 @@ constexpr auto kMaxInlineArea = 1920 * 1080; constexpr auto kSeekAnimationDuration = crl::time(200); constexpr auto kSeekTrackOpacity = 0.2; +using ::Media::ValidFrameSize; + [[nodiscard]] int GifMaxStatusWidth(not_null document) { auto result = st::normalFont->width( Ui::FormatDownloadText(document->size, document->size)); @@ -244,8 +247,7 @@ Gif::~Gif() { } bool Gif::CanPlayInline(not_null document) { - const auto dimensions = document->dimensions; - return dimensions.width() * dimensions.height() <= kMaxInlineArea; + return ValidFrameSize(document->dimensions, kMaxInlineArea); } QSize Gif::sizeForAspectRatio() const { @@ -2292,9 +2294,10 @@ void Gif::repaintStreamedContent() { } void Gif::streamingReady(::Media::Streaming::Information &&info) { - if (info.video.size.width() * info.video.size.height() - > kMaxInlineArea) { - _data->dimensions = info.video.size; + if (!ValidFrameSize(info.video.size, kMaxInlineArea)) { + if (!info.video.size.isEmpty()) { + _data->dimensions = info.video.size; + } stopAnimation(); } else { history()->owner().requestViewResize(_parent); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index 4fe5a064d4..04b48211c4 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_single_player.h" #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" +#include "media/media_common.h" #include "media/player/media_player_instance.h" #include "history/history_location_manager.h" #include "history/view/history_view_cursor_state.h" @@ -39,11 +40,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace InlineBots { namespace Layout { namespace internal { +namespace { using TextState = HistoryView::TextState; constexpr auto kMaxInlineArea = 1280 * 720; +using ::Media::ValidFrameSize; + [[nodiscard]] QSize ScaleDown(int w, int h, int maxW, int maxH) { if (w * maxH > h * maxW) { if (maxH < h) { @@ -60,10 +64,11 @@ constexpr auto kMaxInlineArea = 1280 * 720; } [[nodiscard]] bool CanPlayInline(not_null document) { - const auto dimensions = document->dimensions; - return dimensions.width() * dimensions.height() <= kMaxInlineArea; + return ValidFrameSize(document->dimensions, kMaxInlineArea); } +} // namespace + FileBase::FileBase(not_null context, std::shared_ptr result) : ItemBase(context, std::move(result)) { } @@ -430,10 +435,11 @@ void Gif::clipCallback(Media::Clip::Notification notification) { if (_gif->state() == State::Error) { _gif.setBad(); } else if (_gif->ready() && !_gif->started()) { - if (_gif->width() * _gif->height() > kMaxInlineArea) { - getShownDocument()->dimensions = QSize( - _gif->width(), - _gif->height()); + const auto size = QSize(_gif->width(), _gif->height()); + if (!ValidFrameSize(size, kMaxInlineArea)) { + if (!size.isEmpty()) { + getShownDocument()->dimensions = size; + } _gif.reset(); } else { _gif->start({ @@ -1915,10 +1921,11 @@ void Game::clipCallback(Media::Clip::Notification notification) { if (_gif->state() == State::Error) { _gif.setBad(); } else if (_gif->ready() && !_gif->started()) { - if (_gif->width() * _gif->height() > kMaxInlineArea) { - getResultDocument()->dimensions = QSize( - _gif->width(), - _gif->height()); + const auto size = QSize(_gif->width(), _gif->height()); + if (!ValidFrameSize(size, kMaxInlineArea)) { + if (!size.isEmpty()) { + getResultDocument()->dimensions = size; + } _gif.reset(); } else { _gif->start({ diff --git a/Telegram/SourceFiles/media/clip/media_clip_ffmpeg.cpp b/Telegram/SourceFiles/media/clip/media_clip_ffmpeg.cpp index 67d6df50e4..781e19ebdb 100644 --- a/Telegram/SourceFiles/media/clip/media_clip_ffmpeg.cpp +++ b/Telegram/SourceFiles/media/clip/media_clip_ffmpeg.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/clip/media_clip_ffmpeg.h" #include "core/file_location.h" +#include "media/media_common.h" #include "logs.h" namespace Media { @@ -19,30 +20,10 @@ constexpr auto kSkipInvalidDataPackets = 10; constexpr auto kMaxInlineArea = 1280 * 720; constexpr auto kMaxSendingArea = 3840 * 2160; // usual 4K -// See https://github.com/telegramdesktop/tdesktop/issues/7225 -constexpr auto kAlignImageBy = 64; - -void alignedImageBufferCleanupHandler(void *data) { - auto buffer = static_cast(data); - delete[] buffer; -} - -// Create a QImage of desired size where all the data is aligned to 16 bytes. -QImage createAlignedImage(QSize size) { - auto width = size.width(); - auto height = size.height(); - auto widthalign = kAlignImageBy / 4; - auto neededwidth = width + ((width % widthalign) ? (widthalign - (width % widthalign)) : 0); - auto bytesperline = neededwidth * 4; - auto buffer = new uchar[bytesperline * height + kAlignImageBy]; - auto cleanupdata = static_cast(buffer); - auto bufferval = reinterpret_cast(buffer); - auto alignedbuffer = buffer + ((bufferval % kAlignImageBy) ? (kAlignImageBy - (bufferval % kAlignImageBy)) : 0); - return QImage(alignedbuffer, width, height, bytesperline, QImage::Format_ARGB32_Premultiplied, alignedImageBufferCleanupHandler, cleanupdata); -} - -bool isAlignedImage(const QImage &image) { - return !(reinterpret_cast(image.constBits()) % kAlignImageBy) && !(image.bytesPerLine() % kAlignImageBy); +[[nodiscard]] auto MaxAreaForMode(ReaderImplementation::Mode mode) { + return (mode == ReaderImplementation::Mode::Inspecting) + ? kMaxSendingArea + : kMaxInlineArea; } } // namespace @@ -58,10 +39,8 @@ ReaderImplementation::ReadResult FFMpegReaderImplementation::readNextFrame() { do { int res = avcodec_receive_frame(_codecContext, _frame.get()); if (res >= 0) { - const auto limit = (_mode == Mode::Inspecting) - ? kMaxSendingArea - : kMaxInlineArea; - if (_frame->width * _frame->height > limit) { + const auto limit = MaxAreaForMode(_mode); + if (!::Media::ValidFrameSize(_frame->width, _frame->height, limit)) { return ReadResult::Error; } processReadFrame(); @@ -223,8 +202,12 @@ bool FFMpegReaderImplementation::renderFrame( if (!size.isEmpty() && rotationSwapWidthHeight()) { toSize.transpose(); } - if (to.isNull() || to.size() != toSize || !to.isDetached() || !isAlignedImage(to)) { - to = createAlignedImage(toSize); + if (!FFmpeg::GoodStorageForFrame(to, toSize)) { + to = FFmpeg::CreateFrameStorage(toSize); + if (to.isNull()) { + LOG(("Gif Error: Bad storage size %1").arg(logData())); + return false; + } } const auto format = (_frame->format == AV_PIX_FMT_NONE) ? _codecContext->pix_fmt @@ -346,6 +329,7 @@ bool FFMpegReaderImplementation::start(Mode mode, crl::time &positionMs) { const auto audioStreamId = av_find_best_stream(_fmtContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); _hasAudioStream = (audioStreamId >= 0); } + _codecContext->max_pixels = MaxAreaForMode(_mode); if ((res = avcodec_open2(_codecContext, codec, nullptr)) < 0) { LOG(("Gif Error: Unable to avcodec_open2 %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res))); diff --git a/Telegram/SourceFiles/media/media_common.h b/Telegram/SourceFiles/media/media_common.h index b3049900d7..6cfb6290ce 100644 --- a/Telegram/SourceFiles/media/media_common.h +++ b/Telegram/SourceFiles/media/media_common.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/algorithm.h" #include +#include namespace Media { @@ -42,6 +43,16 @@ inline constexpr auto kSpeedMin = 0.5; inline constexpr auto kSpeedMax = 2.5; inline constexpr auto kSpedUpDefault = 1.7; +[[nodiscard]] inline bool ValidFrameSize(int w, int h, int maxArea) { + return (w > 0) + && (h > 0) + && (int64_t(w) * h <= int64_t(maxArea)); +} + +[[nodiscard]] inline bool ValidFrameSize(QSize size, int maxArea) { + return ValidFrameSize(size.width(), size.height(), maxArea); +} + [[nodiscard]] inline bool EqualSpeeds(float64 a, float64 b) { return int(base::SafeRound(a * 10.)) == int(base::SafeRound(b * 10.)); } diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h index 505f665a02..5b54153bb9 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h @@ -23,6 +23,8 @@ bool SupportsSpeedControl(); namespace Streaming { +inline constexpr auto kMaxFrameArea = 3840 * 2160; + inline bool SupportsSpeedControl() { return Media::Audio::SupportsSpeedControl(); } diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp index 299fdb3555..bb53d30cae 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp @@ -172,6 +172,7 @@ Stream File::Context::initStream( result.codec = FFmpeg::MakeCodecPointer({ .stream = info, .hwAllowed = options.hwAllow, + .videoMaxArea = kMaxFrameArea, }); if (!result.codec) { return result; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp index 68ccc55370..3f6d312dd2 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp @@ -154,6 +154,9 @@ QImage ConvertFrame( if (!FFmpeg::GoodStorageForFrame(storage, resize)) { storage = FFmpeg::CreateFrameStorage(resize); + if (storage.isNull()) { + return QImage(); + } } const auto format = AV_PIX_FMT_BGRA; @@ -429,6 +432,9 @@ QImage PrepareByRequest( : request.outer; if (!FFmpeg::GoodStorageForFrame(storage, outer)) { storage = FFmpeg::CreateFrameStorage(outer); + if (storage.isNull()) { + return QImage(); + } } if (hasAlpha && request.keepAlpha) { diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp index f352f00484..ecb867b5bb 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ffmpeg/ffmpeg_utility.h" #include "media/audio/media_audio.h" +#include "media/media_common.h" #include "base/concurrent_timer.h" #include "core/crash_reports.h" #include "base/debug_log.h" @@ -17,11 +18,12 @@ namespace Media { namespace Streaming { namespace { -constexpr auto kMaxFrameArea = 3840 * 2160; // usual 4K constexpr auto kDisplaySkipped = crl::time(-1); constexpr auto kFinishedPosition = std::numeric_limits::max(); static_assert(kDisplaySkipped != kTimeUnknown); +using ::Media::ValidFrameSize; + [[nodiscard]] QImage ConvertToARGB32( FrameFormat format, const FrameYUV &data) { @@ -35,6 +37,9 @@ static_assert(kDisplaySkipped != kTimeUnknown); //} auto result = FFmpeg::CreateFrameStorage(data.size); + if (result.isNull()) { + return QImage(); + } const auto swscale = FFmpeg::MakeSwscalePointer( data.size, (format == FrameFormat::YUV420 @@ -374,7 +379,11 @@ auto VideoTrackObject::readFrame(not_null frame) -> FrameResult { return FrameResult::Waiting; } const auto decodedFrame = _stream.decodedFrame.get(); - if (int64(decodedFrame->width) * decodedFrame->height > kMaxFrameArea) { + const auto valid = ValidFrameSize( + decodedFrame->width, + decodedFrame->height, + kMaxFrameArea); + if (!valid) { fail(Error::InvalidData); return FrameResult::Error; } @@ -654,7 +663,11 @@ bool VideoTrackObject::tryReadFirstFrame(FFmpeg::Packet &&packet) { bool VideoTrackObject::processFirstFrame() { const auto decodedFrame = _stream.decodedFrame.get(); - if (int64(decodedFrame->width) * decodedFrame->height > kMaxFrameArea) { + const auto valid = ValidFrameSize( + decodedFrame->width, + decodedFrame->height, + kMaxFrameArea); + if (!valid) { return false; } else if (decodedFrame->hw_frames_ctx) { if (!_stream.transferredFrame) { diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index d46518dc3f..79f66b2477 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/file_upload.h" #include "main/main_session.h" #include "media/audio/media_audio.h" +#include "media/media_common.h" #include "media/player/media_player_instance.h" #include "storage/localstorage.h" #include "history/history.h" @@ -65,9 +66,10 @@ TextParseOptions _documentNameOptions = { constexpr auto kMaxInlineArea = 1280 * 720; constexpr auto kStoryRatio = 1.46; +using ::Media::ValidFrameSize; + [[nodiscard]] bool CanPlayInline(not_null document) { - const auto dimensions = document->dimensions; - return dimensions.width() * dimensions.height() <= kMaxInlineArea; + return ValidFrameSize(document->dimensions, kMaxInlineArea); } [[nodiscard]] QImage CropMediaFrame(QImage image, int width, int height) { @@ -2175,10 +2177,11 @@ void Gif::clipCallback(Media::Clip::Notification notification) { if (_gif->state() == State::Error) { _gif.setBad(); } else if (_gif->ready() && !_gif->started()) { - if (_gif->width() * _gif->height() > kMaxInlineArea) { - _data->dimensions = QSize( - _gif->width(), - _gif->height()); + const auto size = QSize(_gif->width(), _gif->height()); + if (!ValidFrameSize(size, kMaxInlineArea)) { + if (!size.isEmpty()) { + _data->dimensions = size; + } _gif.reset(); } else { _gif->start({ From a6c211ceb3ca40bcb1b95a0ec8f92b2e6006e76f Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 11:27:30 +0300 Subject: [PATCH 056/154] Updated lib_ui submodule to fix the build. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 26ea73e65c..cac647adb4 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 26ea73e65cadcce411c6370183d807131f3836bc +Subproject commit cac647adb486ab980073ab1209821617e590aa3b From 923efd9e7ef8ff72d9b83973502e587682119e54 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 13:46:26 +0300 Subject: [PATCH 057/154] Removed private QAction menu role assignments in global menu. --- Telegram/SourceFiles/platform/mac/global_menu_mac.mm | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Telegram/SourceFiles/platform/mac/global_menu_mac.mm b/Telegram/SourceFiles/platform/mac/global_menu_mac.mm index 19390fa11b..0cd600c2b3 100644 --- a/Telegram/SourceFiles/platform/mac/global_menu_mac.mm +++ b/Telegram/SourceFiles/platform/mac/global_menu_mac.mm @@ -354,27 +354,18 @@ void Manager::buildEditMenu(QMenu *edit) { [] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); }, QKeySequence::Cut); _cut->setShortcutContext(Qt::WidgetShortcut); -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - _cut->setMenuRole(QAction::CutRole); -#endif // Qt >= 6.8.0 _copy = edit->addAction( u"Copy"_q, receiver, [] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); }, QKeySequence::Copy); _copy->setShortcutContext(Qt::WidgetShortcut); -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - _copy->setMenuRole(QAction::CopyRole); -#endif // Qt >= 6.8.0 _paste = edit->addAction( u"Paste"_q, receiver, [] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); }, QKeySequence::Paste); _paste->setShortcutContext(Qt::WidgetShortcut); -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - _paste->setMenuRole(QAction::PasteRole); -#endif // Qt >= 6.8.0 _delete = edit->addAction( u"Delete"_q, receiver, @@ -449,9 +440,6 @@ void Manager::buildEditMenu(QMenu *edit) { [] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); }, QKeySequence::SelectAll); _selectAll->setShortcutContext(Qt::WidgetShortcut); -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - _selectAll->setMenuRole(QAction::SelectAllRole); -#endif // Qt >= 6.8.0 if (!Platform::IsMac26_0OrGreater()) { edit->addSeparator(); From 313f2de76269122c1b121386154e39d16a9230a6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 1 May 2026 04:26:02 +0000 Subject: [PATCH 058/154] Removed 114 unused style entries. --- Telegram/SourceFiles/boxes/boxes.style | 32 -------------- Telegram/SourceFiles/boxes/polls.style | 5 --- Telegram/SourceFiles/calls/calls.style | 6 --- .../chat_helpers/chat_helpers.style | 44 ------------------- Telegram/SourceFiles/dialogs/dialogs.style | 3 -- Telegram/SourceFiles/editor/editor.style | 2 - .../boosts/giveaway/giveaway.style | 13 ------ .../earn/channel_earn.style | 4 -- Telegram/SourceFiles/info/info.style | 41 ----------------- Telegram/SourceFiles/intro/intro.style | 1 - Telegram/SourceFiles/iv/iv.style | 8 ---- .../media/player/media_player.style | 5 --- .../SourceFiles/media/view/media_view.style | 3 -- Telegram/SourceFiles/profile/profile.style | 10 ----- Telegram/SourceFiles/settings/settings.style | 10 ----- .../SourceFiles/statistics/statistics.style | 6 --- Telegram/SourceFiles/ui/chat/chat.style | 25 ----------- Telegram/SourceFiles/ui/effects/credits.style | 18 -------- Telegram/SourceFiles/ui/effects/premium.style | 3 -- Telegram/SourceFiles/ui/menu_icons.style | 6 --- Telegram/SourceFiles/window/window.style | 1 - 21 files changed, 246 deletions(-) diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index bf1caf551f..5a1c8cbbd4 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -59,7 +59,6 @@ boxPhotoTitlePosition: point(28px, 20px); boxPhotoPadding: margins(28px, 28px, 28px, 18px); boxPhotoCompressedSkip: 20px; boxPhotoCaptionSkip: 8px; -boxPhotoCaptionReplyOverlap: 5px; defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }}; defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }}; @@ -115,10 +114,8 @@ confirmInviteStatus: FlatLabel(confirmInviteAbout) { } confirmInviteAboutPadding: margins(36px, 4px, 36px, 10px); confirmInviteAboutRequestsPadding: margins(36px, 9px, 36px, 15px); -confirmInviteTitleTop: 141px; confirmInvitePhotoSize: 96px; confirmInvitePhotoTop: 33px; -confirmInviteStatusTop: 164px; confirmInviteUserHeight: 100px; confirmInviteUserPhotoSize: 50px; confirmInviteUserPhotoTop: 210px; @@ -349,7 +346,6 @@ themeWarningHeight: 150px; themeWarningTextTop: 60px; aboutWidth: 390px; -aboutVersionTop: -3px; aboutVersionLink: LinkButton(defaultLinkButton) { color: windowSubTextFg; overColor: windowSubTextFg; @@ -386,7 +382,6 @@ connectionUserInputField: InputField(defaultInputField) { } connectionPasswordInputField: InputField(defaultInputField) { } -connectionIPv6Skip: 11px; autolockWidth: 256px; autolockButton: Checkbox(defaultBoxCheckbox) { @@ -529,7 +524,6 @@ colorResultInput: InputField(colorValueInput) { changePhoneButton: RoundButton(defaultActiveButton) { width: 256px; } -changePhoneButtonPadding: margins(0px, 32px, 0px, 44px); changePhoneTitle: FlatLabel(boxTitle) { } changePhoneTitlePadding: margins(0px, 8px, 0px, 8px); @@ -550,18 +544,6 @@ changePhoneError: FlatLabel(changePhoneLabel) { normalBoxLottieSize: size(120px, 120px); -adminLogFilterUserpicLeft: 15px; -adminLogFilterLittleSkip: 16px; -adminLogFilterCheckbox: Checkbox(defaultBoxCheckbox) { - style: TextStyle(boxTextStyle) { - font: font(boxFontSize semibold); - } -} -adminLogFilterSkip: 32px; -adminLogFilterUserCheckbox: Checkbox(defaultBoxCheckbox) { - margin: margins(8px, 6px, 8px, 6px); - checkPosition: point(8px, 6px); -} rightsCheckbox: Checkbox(defaultCheckbox) { textPosition: point(10px, 1px); rippleBg: attentionButtonBgOver; @@ -588,7 +570,6 @@ rightsButtonToggleWidth: 70px; rightsDividerMargin: margins(0px, 0px, 0px, 20px); rightsHeaderMargin: margins(22px, 13px, 22px, 7px); rightsToggleMargin: margins(22px, 8px, 22px, 8px); -rightsAboutMargin: margins(22px, 8px, 22px, 8px); rightsPhotoButton: UserpicButton(defaultUserpicButton) { size: size(60px, 60px); photoSize: 60px; @@ -1001,8 +982,6 @@ contactsWithStories: PeerList(peerListBox) { nameFgChecked: contactsNameFg; } } -storiesReadLineTwice: 2px; -storiesUnreadLineTwice: 4px; requestsAcceptButton: RoundButton(defaultActiveButton) { width: -28px; height: 30px; @@ -1087,7 +1066,6 @@ collectibleHeaderPadding: margins(24px, 16px, 24px, 12px); collectibleOwnerPadding: margins(24px, 4px, 24px, 8px); collectibleInfo: inviteForbiddenInfo; collectibleInfoPadding: margins(24px, 12px, 24px, 12px); -collectibleInfoTonMargins: margins(0px, 3px, 0px, 0px); collectibleMore: RoundButton(defaultActiveButton) { height: 36px; textTop: 9px; @@ -1134,7 +1112,6 @@ moderateBoxDividerLabel: FlatLabel(boxDividerLabel) { } profileQrFont: font(fsize bold); -profileQrCenterSize: 34px; profileQrBackgroundRadius: 12px; profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }}; profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px); @@ -1147,13 +1124,6 @@ foldersMenu: PopupMenu(popupMenuWithIcons) { } } -fakeUserpicButton: UserpicButton(defaultUserpicButton) { - size: size(1px, 1px); - photoSize: 1px; - changeIcon: icon {{ "settings/photo", transparent }}; - uploadBg: transparent; -} - moderateCommonGroupsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { imageRadius: 12px; imageSmallRadius: 11px; @@ -1213,7 +1183,6 @@ createBotStatusLabel: FlatLabel(aboutRevokePublicLabel) { maxHeight: 20px; } -aiComposeBoxSectionSkip: 8px; aiComposeBoxStyleTabsSkip: 8px; aiComposeContentMargin: margins(16px, 0px, 16px, 0px); @@ -1282,7 +1251,6 @@ aiComposeCardRadius: 22px; aiComposeCardPadding: margins(12px, 16px, 16px, 16px); aiComposeCardDivider: shadowFg; aiComposeCardSectionSkip: 12px; -aiComposeCardTextSkip: 0px; aiComposeCardControlSkip: 8px; aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) { textFg: windowFg; diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index 960a49a5b8..b68834fc16 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -77,7 +77,6 @@ historyPollExplanationTitleSkip: 4px; historyPollExplanationCloseSize: 20px; historyPollExplanationCloseIconSize: 8px; historyPollExplanationCloseStroke: 2px; -historyPollExplanationMediaMaxHeight: 300px; historyPollExplanationMediaSkip: 6px; historyPollChoiceRight: icon {{ "poll_choice_right", activeButtonFg }}; historyPollChoiceWrong: icon {{ "poll_choice_wrong", activeButtonFg }}; @@ -91,7 +90,6 @@ historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }}; historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }}; historyPollAddOptionTop: 3px; -historyPollAddOptionHeight: 32px; historyPollAddOptionButtonSize: 26px; historyPollAddOptionEmojiLeft: -4px; historyPollAddOptionField: InputField(defaultInputField) { @@ -129,9 +127,7 @@ historyPollAddOptionAttach: IconButton(defaultIconButton) { ripple: defaultRippleAnimationBgOver; } -pollBoxOutlinePollEmojiIcon: icon{{ "poll/general/outline_poll_emoji", activeButtonFg }}; pollBoxOutlinePollAddIcon: icon{{ "poll/general/outline_poll_add-18x18", activeButtonFg }}; -pollBoxOutlinePollAttachIcon: icon{{ "poll/general/outline_poll_attach", activeButtonFg }}; pollBoxMenuPollOrderIcon: icon{{ "poll/general/menu_poll_order-24x24", historyComposeIconFg }}; pollBoxFilledPollDeadlineIcon: icon{{ "poll/filled/filled_poll_deadline", activeButtonFg }}; pollBoxFilledPollViewIcon: icon{{ "poll/filled/filled_poll_view", activeButtonFg }}; @@ -145,7 +141,6 @@ pollAttachTextSkip: 28px; pollAttachProgressMargin: 4px; pollAttachCancel: icon {{ "history_audio_cancel", historyFileThumbIconFg }}; pollAttachView: icon {{ "mediaview/views", historyFileThumbIconFg }}; -pollAttachShift: point(-11px, -2px); createPollField: InputField(defaultInputField) { textMargins: margins(0px, 4px, 0px, 4px); diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index b15fbd6167..cb6c742798 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -538,10 +538,6 @@ callTitle: WindowTitle(defaultWindowTitle) { callTitleShadowRight: icon {{ "calls/calls_shadow_controls", windowShadowFg }}; callTitleShadowLeft: icon {{ "calls/calls_shadow_controls-flip_horizontal", windowShadowFg }}; -callErrorToast: Toast(defaultToast) { - minWidth: 240px; -} - groupCallWidth: 380px; groupCallHeight: 520px; groupCallWidthRtmp: 720px; @@ -1488,8 +1484,6 @@ groupCallAttentionBoxButton: RoundButton(groupCallBoxButton) { groupCallRtmpUrlSkip: 1px; groupCallRtmpKeySubsectionTitleSkip: 8px; groupCallRtmpSubsectionTitleAddPadding: margins(0px, -1px, 0px, -4px); -groupCallRtmpShowButtonPosition: point(21px, -5px); -groupCallDividerBg: groupCallMembersBgRipple; groupCallScheduleDateField: InputField(groupCallField) { textMargins: margins(2px, 0px, 2px, 0px); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index dc1dbeadab..92daf29f68 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -447,7 +447,6 @@ stickersAddCellBgRadius: 28px; stickersEmojiPickerExpandedRadius: 20px; stickersEmojiPickerBg: emojiPanBg; -stickersEmojiPickerShadow: windowShadowFg; stickersEmojiPickerPadding: margins(12px, 8px, 12px, 0px); stickersEmojiPickerItemSize: 30px; stickersEmojiPickerItemSkip: 4px; @@ -459,8 +458,6 @@ stickersEmojiPickerStripBubble: icon{ }; stickersEmojiPickerStripBubbleRight: 20px; stickersEmojiPickerSelectedBg: windowBgActive; -stickersEmojiPickerSelectedFg: windowBgActive; -stickersEmojiPickerHeaderFg: windowSubTextFg; stickersEmojiPickerScroll: ScrollArea(boxScroll) { width: 14px; deltax: 5px; @@ -475,14 +472,6 @@ stickersEmojiPickerAbout: FlatLabel(defaultFlatLabel) { font: font(12px); } } -stickersEmojiPickerSectionHeader: FlatLabel(defaultFlatLabel) { - minWidth: 10px; - align: align(topleft); - textFg: windowSubTextFg; - style: TextStyle(defaultTextStyle) { - font: font(12px semibold); - } -} stickersEmojiPickerExpandIcon: icon {{ "intro_country_dropdown", windowSubTextFg }}; stickersEmojiPickerCollapseIcon: icon {{ "intro_country_dropdown-flip_vertical", windowSubTextFg }}; stickersEmojiPickerExpandSize: 24px; @@ -1011,10 +1000,6 @@ historyComposeButton: FlatButton { color: historyComposeButtonBgRipple; } } -historyComposeButtonText: FlatLabel(defaultFlatLabel) { - style: semiboldTextStyle; - textFg: windowActiveTextFg; -} historyGiftToChannel: IconButton(defaultIconButton) { width: 46px; height: 46px; @@ -1293,7 +1278,6 @@ historyAddMedia: IconButton(historyAttach) { } historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }}; -historyEmojiCircle: size(20px, 20px); historyEmojiCircleLine: 1.5; historyEmojiCircleFg: historyComposeIconFg; historyEmojiCircleFgOver: historyComposeIconFgOver; @@ -1330,25 +1314,6 @@ historySuggestPostToggle: IconButton(historyAttach) { historySuggestIconPosition: point(4px, 4px); historySuggestIconActive: icon{{ "chat/input_paid", windowActiveTextFg }}; -suggestOptionsPrice: InputField(defaultInputField) { - textBg: transparent; - textMargins: margins(2px, 20px, 2px, 0px); - - placeholderFg: placeholderFg; - placeholderFgActive: placeholderFgActive; - placeholderFgError: placeholderFgActive; - placeholderMargins: margins(2px, 0px, 2px, 0px); - placeholderScale: 0.; - placeholderFont: normalFont; - - border: 0px; - borderActive: 0px; - - heightMin: 32px; - - style: defaultTextStyle; -} - historyAttachEmojiInner: IconButton(historyAttach) { icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; iconOver: icon {{ "chat/input_smile_face", historyComposeIconFgOver }}; @@ -1400,9 +1365,7 @@ historyRecordVoiceFgInactive: attentionButtonFg; historyRecordVoiceFgActive: windowBgActive; historyRecordVoiceFgActiveIcon: windowFgActive; 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 }}; historyRecordSendIconPosition: point(2px, 0px); historyRecordVoiceRippleBgActive: lightButtonBgOver; @@ -1412,7 +1375,6 @@ historyRecordCancelActive: historySendIconFg; historyRecordFont: font(13px); historyRecordDurationSkip: 12px; historyRecordDurationFg: historyComposeAreaFg; -historyRecordTTLLineWidth: 2px; historyRecordMainBlobMinRadius: 23px; historyRecordMainBlobMaxRadius: 37px; @@ -1457,7 +1419,6 @@ historyRecordDelete: IconButton(historyAttach) { iconOver: icon {{ "voice_lock/recorded_delete", historyComposeIconFgOver }}; iconPosition: point(10px, 11px); } -historyRecordWaveformRightSkip: 10px; historyRecordWaveformBgMargins: margins(5px, 8px, 5px, 9px); historyRecordWaveformBgRadius: 7px; historyRecordWaveformOutsideAlpha: 0.6; @@ -1469,16 +1430,12 @@ historyRecordCenterControlTextSkip: 2px; historyRecordCenterControlMinimumProgressPadding: 5px; historyRecordWaveformBar: 3px; -historyRecordTrimFrameRadius: 5px; -historyRecordTrimFrameBorder: 1px; historyRecordTrimHandleWidth: 10px; historyRecordTrimHandleInnerSize: size(2px, 8px); -historyRecordTrimHandleInnerSkip: 2px; historyRecordLockPosition: point(1px, 22px); historyRecordCancelButtonWidth: 100px; -historyRecordCancelButtonFg: lightButtonFg; historyRecordTooltipSkip: 8px; historyRecordTooltip: ImportantTooltip(defaultImportantTooltip) { @@ -1557,7 +1514,6 @@ importantTooltipHide: IconButton(defaultIconButton) { ripple: emptyRippleAnimation; } boxAiComposeButtonPosition: point(0px, -4px); -historyRecordFrameIndex: 30; defaultComposeFilesMenu: IconButton(defaultIconButton) { width: 48px; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 2878c7fc38..f7f621e6c3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -531,9 +531,6 @@ dialogsSearchInMenu: PopupMenu(defaultPopupMenu) { dialogsSearchInCheck: icon {{ "player/player_check", mediaPlayerActiveFg }}; dialogsSearchInCheckSkip: 8px; dialogsSearchFromStyle: defaultTextStyle; -dialogsSearchFromPalette: TextPalette(defaultTextPalette) { - linkFg: dialogsNameFg; -} dialogsScamPadding: margins(2px, 0px, 2px, 0px); dialogsScamFont: font(9px semibold); diff --git a/Telegram/SourceFiles/editor/editor.style b/Telegram/SourceFiles/editor/editor.style index f5ede44bc5..074e936849 100644 --- a/Telegram/SourceFiles/editor/editor.style +++ b/Telegram/SourceFiles/editor/editor.style @@ -42,7 +42,6 @@ photoEditorTextButtonPadding: margins(22px, 0px, 22px, 0px); photoEditorButtonStyle: TextStyle(semiboldTextStyle) { font: font(14px semibold); } -photoEditorButtonTextTop: 15px; photoEditorAbout: FlatLabel(defaultFlatLabel) { textFg: mediaviewCaptionFg; @@ -129,7 +128,6 @@ photoEditorColorButtonBorderFg: mediaviewCaptionFg; photoEditorColorPaletteItemSize: 20px; photoEditorColorPaletteGap: 6px; photoEditorColorPaletteSelectionWidth: 2px; -photoEditorColorPaletteSelectionFg: mediaviewCaptionFg; photoEditorColorPalettePlusLine: 2px; photoEditorColorPalettePlusFg: mediaviewCaptionFg; diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index 86207a02e2..ba4e32f69a 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -185,10 +185,6 @@ giveawayRefundedPadding: margins(8px, 10px, 8px, 10px); startGiveawayBox: Box(premiumGiftBox) { shadowIgnoreTopSkip: true; } -startGiveawayScrollArea: ScrollArea(boxScroll) { - deltax: 3px; - deltat: 50px; -} startGiveawayBoxTitleClose: IconButton(boxTitleClose) { ripple: universalRippleAnimation; } @@ -213,12 +209,6 @@ startGiveawayButtonLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimati thickness: 2px; } -starConvertButtonLoading: InfiniteRadialAnimation(startGiveawayButtonLoading) { - color: windowActiveTextFg; - thickness: 2px; -} - -starGiftSmallButton: defaultTableSmallButton; darkGiftCodeBox: Box(giveawayGiftCodeBox) { bg: groupCallMembersBg; title: FlatLabel(boxTitle) { @@ -274,9 +264,6 @@ darkGiftTableMessage: FlatLabel(giveawayGiftMessage) { textFg: groupCallMembersFg; palette: darkGiftPalette; } -darkGiftCodeLink: FlatLabel(giveawayGiftCodeLink) { - textFg: mediaviewMenuFg; -} darkGiftBoxClose: IconButton(boxTitleClose) { icon: icon {{ "box_button_close", groupCallMemberInactiveIcon }}; iconOver: icon {{ "box_button_close", groupCallMemberInactiveIcon }}; diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style index f85b36d29d..393f0661be 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style +++ b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style @@ -9,8 +9,6 @@ using "ui/basic.style"; using "boxes/boxes.style"; using "statistics/statistics.style"; -channelEarnLearnArrowMargins: margins(-2px, 5px, 0px, 0px); - channelEarnOverviewTitleSkip: 11px; channelEarnOverviewMajorLabel: FlatLabel(defaultFlatLabel) { maxHeight: 30px; @@ -112,7 +110,6 @@ channelEarnBalanceMinorLabel: FlatLabel(channelEarnOverviewMinorLabel) { } } channelEarnBalanceMinorLabelSkip: 6px; -channelEarnFadeDuration: 60; channelEarnLearnDescription: FlatLabel(defaultFlatLabel) { maxHeight: 0px; @@ -152,4 +149,3 @@ botEarnButtonLock: IconEmoji { icon: icon{{ "chat/mini_lock", premiumButtonFg }}; padding: margins(-2px, 4px, 0px, 0px); } - diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index f3d0b09f01..0a7b4b191c 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -65,15 +65,6 @@ defaultSubTabs: SubTabs { infoMediaHeaderFg: windowFg; -infoToggle: InfoToggle { - color: menuIconFg; - duration: slideWrapDuration; - size: 24px; - skip: 5px; - stroke: 2px; - rippleAreaPadding: 8px; -} - infoMediaSearch: SearchFieldRow { height: 44px; padding: margins(8px, 6px, 8px, 6px); @@ -371,16 +362,6 @@ infoTopBarColoredClose: IconButton(infoTopBarClose) { iconOver: icon {{ "info/info_close", groupCallMembersFg }}; ripple: universalRippleAnimation; } -infoTopBarColoredMenu: IconButton(infoTopBarMenu) { - icon: icon {{ "title_menu_dots", groupCallMembersFg }}; - iconOver: icon {{ "title_menu_dots", groupCallMembersFg }}; - ripple: universalRippleAnimation; -} -infoLayerTopBarColoredMenu: IconButton(infoLayerTopBarMenu) { - icon: icon {{ "title_menu_dots", groupCallMembersFg }}; - iconOver: icon {{ "title_menu_dots", groupCallMembersFg }}; - ripple: universalRippleAnimation; -} infoTopBarColoredEdit: IconButton(infoTopBarEdit) { icon: icon {{ "menu/edit", groupCallMembersFg }}; iconOver: icon {{ "menu/edit", groupCallMembersFg }}; @@ -594,7 +575,6 @@ infoEditContactCover: InfoProfileCover(infoProfileCover) { nameTop: 33px; statusTop: 57px; } -infoEditContactPersonalLeft: 6px; infoRatingDeductedBadge: RoundButton(customEmojiTextBadge) { textBg: windowSubTextFg; @@ -649,8 +629,6 @@ infoProfileLabeledButtonQrInset: 5px; infoIconInformation: icon {{ "info/info_information", infoIconFg }}; infoIconAddMember: icon {{ "info/info_add_member", infoIconFg }}; -infoIconBotBalance: icon {{ "menu/earn", infoIconFg, point(5px, 5px) }}; -infoIconNotifications: icon {{ "info/info_notifications", infoIconFg }}; infoIconMediaPhoto: icon {{ "info/info_media_photo", infoIconFg }}; infoIconMediaVideo: icon {{ "info/info_media_video", infoIconFg }}; infoIconMediaGif: icon {{ "info/info_media_gif", infoIconFg }}; @@ -679,8 +657,6 @@ infoIconBlock: icon {{ "info/info_block", attentionButtonFg }}; infoIconMembers: icon {{ "info/info_members", infoIconFg }}; infoIconPrivacyPolicy: icon {{ "menu/2sv_off", infoIconFg, point(4px, 4px) }}; infoIconSettings: icon {{ "menu/manage", infoIconFg, point(4px, 4px) }}; -infoInformationIconPosition: point(25px, 12px); -infoNotificationsIconPosition: point(20px, 5px); infoSharedMediaButtonIconPosition: point(20px, 3px); infoGroupMembersIconPosition: point(20px, 10px); infoChannelMembersIconPosition: point(20px, 4px); @@ -968,7 +944,6 @@ editPeerTopButtonsLayoutSkipCustomBottom: 5px; editPeerHistoryVisibilityTopSkip: 8px; editPeerPhotoMargins: margins(22px, 8px, 22px, 8px); -editPeerTitle: defaultInputField; editPeerTitleMargins: margins(27px, 13px, 22px, 8px); editPeerTitleEmojiPosition: point(0px, 23px); editPeerTitleField: InputField(defaultInputField) { @@ -1004,14 +979,6 @@ editPeerPrivacyBoxCheckbox: Checkbox(defaultBoxCheckbox) { editPeerPrivacyLabelMargins: margins(42px, 0px, 34px, 0px); editPeerPreHistoryLabelMargins: margins(34px, 0px, 34px, 0px); editPeerUsernameFieldMargins: margins(22px, 0px, 22px, 0px); -editPeerUsername: setupChannelLink; -editPeerUsernameGood: FlatLabel(defaultFlatLabel) { - textFg: boxTextFgGood; - style: boxTextStyle; -} - -editPeerReactionsPreview: 24px; -editPeerReactionsIconLeft: 21px; historyTopBarBack: IconButton(infoTopBarBack) { width: 52px; @@ -1022,10 +989,6 @@ topBarMenuGroupCallSkip: 20px; topBarBack: icon {{ "title_back", lightButtonFg }}; topBarArrowPadding: margins(39px, 8px, 17px, 8px); topBarNameRightPadding: 3px; -topBarButton: RoundButton(defaultLightButton) { - width: -18px; - padding: margins(0px, 10px, 12px, 10px); -} topBarClearButton: RoundButton(defaultLightButton) { width: -18px; } @@ -1320,10 +1283,6 @@ similarChannelsLock: RoundButton(defaultActiveButton) { textTop: 12px; style: semiboldTextStyle; } -similarChannelsLockLabel: FlatLabel(defaultFlatLabel) { - textFg: premiumButtonFg; - style: semiboldTextStyle; -} similarChannelsLockPadding: margins(12px, 12px, 12px, 12px); similarChannelsLockAbout: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; diff --git a/Telegram/SourceFiles/intro/intro.style b/Telegram/SourceFiles/intro/intro.style index ddf09c24b6..959f453dd6 100644 --- a/Telegram/SourceFiles/intro/intro.style +++ b/Telegram/SourceFiles/intro/intro.style @@ -113,7 +113,6 @@ introPhone: InputField(introCountry) { width: 225px; } introQrLoginLinkTop: 368px; -introCode: introCountry; introName: introCountry; introPassword: introCountry; introPasswordTop: 74px; diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index a3129f5d29..a162e3df51 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -28,14 +28,6 @@ ivBack: IconButton(ivMenuToggle) { rippleAreaPosition: point(12px, 6px); } ivZoomButtonsSize: 26px; -ivPlusMinusZoom: IconButton(ivMenuToggle) { - width: ivZoomButtonsSize; - height: ivZoomButtonsSize; - - rippleAreaPosition: point(0px, 0px); - rippleAreaSize: ivZoomButtonsSize; - ripple: defaultRippleAnimationBgOver; -} ivResetZoomStyle: TextStyle(defaultTextStyle) { font: font(12px); } diff --git a/Telegram/SourceFiles/media/player/media_player.style b/Telegram/SourceFiles/media/player/media_player.style index 541f073963..708346f178 100644 --- a/Telegram/SourceFiles/media/player/media_player.style +++ b/Telegram/SourceFiles/media/player/media_player.style @@ -298,11 +298,6 @@ mediaPlayerPanelMarginLeft: 10px; mediaPlayerPanelMarginBottom: 10px; mediaPlayerPanelWidth: 344px; -mediaPlayerPanelNextButton: IconButton(mediaPlayerRepeatButton) { - width: 37px; - icon: icon {{ "player/player_forward", mediaPlayerActiveFg, point(6px, 4px) }}; -} - mediaPlayerPanelPlaybackPadding: 8px; mediaPlayerPanelPlayback: defaultContinuousSlider; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 526ca28306..cccad859cd 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -80,7 +80,6 @@ mediaviewPipButton: IconButton(mediaviewControlsButton) { iconPosition: point(4px, 4px); } -mediaviewPipMargins: callShadowExtend; mediaviewPipShadow: callShadow; mediaviewFullScreenButtonSkip: 4px; @@ -470,8 +469,6 @@ pipVolumeIcon1Over: icon {{ "player/player_volume_small", mediaviewPipControlsFg pipVolumeIcon2: icon {{ "player/player_volume_on", mediaviewPipControlsFg }}; pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOver }}; -speedSliderDividerSize: size(2px, 8px); - storiesMaxSize: size(540px, 960px); storiesSiblingWidthMin: 200px; // Try making sibling not less than this. storiesMaxNameFontSize: 17px; diff --git a/Telegram/SourceFiles/profile/profile.style b/Telegram/SourceFiles/profile/profile.style index b58d5f6d1b..0e535f334f 100644 --- a/Telegram/SourceFiles/profile/profile.style +++ b/Telegram/SourceFiles/profile/profile.style @@ -30,13 +30,3 @@ profileBlockTitleHeight: 24px; profileBlockTitleFont: font(14px semibold); profileBlockTitleFg: windowBoldFg; profileBlockTitlePosition: point(24px, 0px); -profileBlockTextPart: FlatLabel(defaultFlatLabel) { - minWidth: 180px; - margin: margins(5px, 5px, 5px, 5px); -} -profileBlockOneLineTextPart: FlatLabel(profileBlockTextPart) { - minWidth: 0px; // No need to set minWidth in one-line text. - maxHeight: 20px; -} - -profileMemberNameFg: windowBoldFg; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 47ae190fe4..8c05a1d8d9 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -182,10 +182,6 @@ settingsPrivacySkip: 14px; settingsPrivacySkipTop: 4px; settingsPrivacyPremium: icon{{ "profile_premium", premiumButtonFg }}; -settingsPrivacyAddBirthday: FlatLabel(defaultFlatLabel) { - minWidth: 256px; -} - settingsCloudPasswordIconSize: 100px; settingLocalPasscodeInputField: InputField(defaultInputField) { @@ -194,7 +190,6 @@ settingLocalPasscodeInputField: InputField(defaultInputField) { settingLocalPasscodeDescription: FlatLabel(changePhoneDescription) { minWidth: 256px; } -settingLocalPasscodeDescriptionHeight: 53px; settingLocalPasscodeError: FlatLabel(changePhoneError) { minWidth: 256px; } @@ -351,8 +346,6 @@ dictionariesSectionButton: SettingsButton(settingsUpdateToggle) { } } -sessionsScroll: boxScroll; -sessionsHeight: 350px; sessionLocationTop: 54px; sessionCurrentSkip: 8px; sessionSubtitleSkip: 14px; @@ -737,8 +730,6 @@ settingsGiftIconEmoji: IconEmoji { padding: margins(1px, 2px, 1px, 0px); } -settingsCreditsButtonBuyIcon: icon {{ "settings/add", windowBgActive, point(7px, 0px) }}; - settingsCreditsButton: SettingsButton(settingsButton) { padding: margins(62px, 8px, 22px, 8px); } @@ -796,4 +787,3 @@ detailedSettingsButtonStyle: DetailedSettingsButtonStyle { } settingsToastStarIcon: icon {{ "toast/star", toastFg }}; - diff --git a/Telegram/SourceFiles/statistics/statistics.style b/Telegram/SourceFiles/statistics/statistics.style index 8f7395a340..97b2180d27 100644 --- a/Telegram/SourceFiles/statistics/statistics.style +++ b/Telegram/SourceFiles/statistics/statistics.style @@ -99,7 +99,6 @@ statisticsOverviewSubtext: FlatLabel(boxLabel) { statisticsOverviewMidSkip: 50px; statisticsOverviewRightSkip: 14px; -statisticsRecentPostRowHeight: 40px; statisticsRecentPostButton: SettingsButton(defaultSettingsButton) { height: 56px; padding: margins(7px, 0px, 24px, 0px); @@ -145,10 +144,6 @@ boostsListBox: PeerList(defaultPeerList) { } boostsLinkSkip: 5px; boostsLinkFieldPadding: margins(22px, 7px, 22px, 12px); -boostsButton: SettingsButton(defaultSettingsButton) { - textFg: lightButtonFg; - textFgOver: lightButtonFgOver; -} getBoostsButton: SettingsButton(reportReasonButton) { textFg: lightButtonFg; @@ -178,6 +173,5 @@ boostsListGiftMiniIcon: icon{{ "boosts/mini_gift", historyPeer8UserpicBg2 }}; boostsListGiveawayMiniIcon: icon{{ "boosts/mini_giveaway", historyPeer4UserpicBg2 }}; boostsListUnclaimedIcon: icon{{ "boosts/boost_unclaimed", premiumButtonFg }}; boostsListUnknownIcon: icon{{ "boosts/boost_unknown", premiumButtonFg }}; -boostsListCreditsIconSize: 13px; statisticsCurrencyIcon: icon {{ "statistics/mini_currency_graph", windowSubTextFg }}; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 724aca8104..638affe794 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -222,9 +222,6 @@ maxWallPaperWidth: 160px; maxWallPaperHeight: 240px; historyThemeSize: size(272px, 176px); -extendedPreviewButtonPadding: margins(20px, 10px, 20px, 10px); -extendedPreviewButtonMargin: 20px; - historyMinimalWidth: 380px; reactionMenu: PopupMenu(defaultPopupMenu) { @@ -282,7 +279,6 @@ membersInnerDropdown: InnerDropdown(defaultInnerDropdown) { scrollMargin: margins(0px, 5px, 0px, 5px); scrollPadding: margins(0px, 3px, 0px, 3px); } -membersInnerItem: defaultPeerListItem; historyFileOutImage: icon {{ "history_file_image", historyFileOutIconFg }}; historyFileOutImageSelected: icon {{ "history_file_image", historyFileOutIconFgSelected }}; @@ -682,7 +678,6 @@ webPageDescriptionStyle: defaultTextStyle; webPagePhotoDelta: 8px; webPageAuctionTimerPadding: margins(8px, 4px, 8px, 4px); webPageAuctionTimeFont: font(11px); -webPageAuctionSubtextFont: font(12px); webPageAuctionPreviewPadding: margins(10px, 30px, 10px, 5px); historyChecklistTaskPadding: margins(32px, 12px, 0px, 12px); @@ -894,9 +889,7 @@ reactionInlineUserpics: GroupCallUserpics { } reactionInfoSize: 15px; -reactionInfoImage: 30px; reactionInfoSkip: 3px; -reactionInfoDigitSkip: 6px; reactionInfoBetween: 3px; reactionCornerSize: size(36px, 32px); @@ -1058,7 +1051,6 @@ storyMentionReadStrokeTwice: 3px; storyMentionButtonSkip: 5px; chatGiveawayWidth: 292px; -chatGiveawayStickerTop: -16px; chatGiveawayStickerPadding: margins(14px, -2px, 14px, 14px); chatGiveawayWinnersTopSkip: 25px; chatGiveawayBadgeFont: font(12px bold); @@ -1078,7 +1070,6 @@ chatGiveawayPeerPadding: margins(5px, 7px, 12px, 0px); chatGiveawayPeerSkip: 8px; chatGiveawayCreditsIconHeight: 19px; -chatSimilarRadius: 12px; chatSimilarArrowSize: 6px; chatSimilarTitle: semiboldFont; chatSimilarTitlePosition: point(15px, 9px); @@ -1124,7 +1115,6 @@ chatSuggestChangeIcon: IconEmoji { premiumRequiredWidth: 186px; premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }}; -premiumRequiredCircle: 60px; directMessagesIcon: icon{{ "chat/large_messages", msgServiceFg }}; starsPerMessageWidth: 226px; @@ -1442,20 +1432,6 @@ suggestPriceEstimate: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } suggestPriceEstimateTop: 12px; -tonInput: InputField(defaultInputField) { - textBg: transparent; - textMargins: margins(0px, 7px, 0px, 7px); - - placeholderFg: placeholderFg; - placeholderFgActive: placeholderFgActive; - placeholderFgError: placeholderFgActive; - placeholderMargins: margins(0px, 0px, 0px, 0px); - placeholderScale: 0.; - placeholderFont: boxTextFont; - - heightMin: 34px; - heightMax: 100px; -} starsFieldIconPosition: point(0px, 10px); tonFieldIconSize: 16px; tonFieldIconPosition: point(2px, 11px); @@ -1552,7 +1528,6 @@ historySummaryHeaderIconSize: 30px; historySummaryHeaderIconSizeInner: 5px; toastCheckIcon: icon {{ "toast/check-36x36", toastFg }}; -toastCheckIconPadding: margins(12px, 8px, 12px, 8px); historySummaryStars: icon {{ "chat/summary_stars-24x24", msgServiceFg }}; historySummaryArrows: icon {{ "chat/summary_arrows-24x24", msgServiceFg }}; diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index e162274617..09490e5ee7 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -11,23 +11,12 @@ using "ui/widgets/widgets.style"; using "ui/effects/premium.style"; using "settings/settings.style"; -creditsSettingsBigBalance: FlatLabel(defaultFlatLabel) { - style: TextStyle(defaultTextStyle) { - font: font(24px semibold); - } -} -creditsSettingsBigBalanceSkip: 4px; creditsSettingsBigBalanceButton: RoundButton(defaultActiveButton) { width: 240px; height: 40px; textTop: 11px; style: semiboldTextStyle; } -creditsSettingsBigBalanceButtonGift: RoundButton(defaultLightButton) { - height: 42px; - textTop: 12px; - style: defaultTextStyle; -} creditsPremiumCover: PremiumCover(defaultPremiumCover) { starTopSkip: 39px; @@ -210,8 +199,6 @@ giftBoxByStarsStyle: TextStyle(defaultTextStyle) { } giftBoxByStarsSkip: 2px; giftBoxByStarsStarTop: 3px; -giftBoxPremiumIconSize: 64px; -giftBoxPremiumIconTop: 10px; giftBoxPremiumTextTop: 84px; giftBoxPremiumTextTopByStars: 78px; giftBoxButtonBottomSmall: 4px; @@ -309,8 +296,6 @@ uniqueGiftNumber: FlatLabel(uniqueGiftTitle) { uniqueGiftResalePrice: FlatLabel(defaultFlatLabel) { style: semiboldTextStyle; } -uniqueGiftResalePadding: margins(4px, 4px, 8px, 4px); -uniqueGiftResaleMargin: margins(10px, 10px, 10px, 10px); uniqueGiftTitleTop: 140px; uniqueGiftSubtitle: FlatLabel(defaultFlatLabel) { minWidth: 64px; @@ -361,7 +346,6 @@ uniqueGiftSenderBadge: FlatLabel(defaultFlatLabel) { } uniqueAttributeTop: 10px; -uniqueAttributeSkip: 8px; uniqueAttributePadding: margins(6px, 8px, 6px, 8px); uniqueAttributeName: TextStyle(semiboldTextStyle) { font: font(12px semibold); @@ -549,8 +533,6 @@ videoStreamStarsCover: PremiumCover(creditsLowBalancePremiumCover) { } } -auctionInfoPreviewMargin: margins(0px, 24px, 0px, 8px); -auctionInfoSubtitleSkip: 8px; auctionInfoTableMargin: margins(0px, 12px, 0px, 12px); auctionInfoValueMultiline: FlatLabel(defaultTableValue) { minWidth: 96px; diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 85677accc3..794359f9ca 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -464,7 +464,6 @@ paidReactToastLabel: FlatLabel(defaultFlatLabel) { palette: defaultToastPalette; } paidReactTopStarIcon: icon{{ "chat/mini_stars", premiumButtonFg }}; -paidReactTopStarIconPosition: point(0px, 1px); paidReactTopStarSkip: 4px; paidReactChannelArrow: icon{{ "intro_country_dropdown", activeButtonFg }}; paidReactChannelMenu: PopupMenu(popupMenuWithIcons) { @@ -548,8 +547,6 @@ starrefLinkCountIcon: icon{{ "chat/mini_subscribers", historyPeerUserpicFg }}; starrefLinkCountIconPosition: point(0px, 1px); starrefLinkCountFont: font(10px bold); starrefLinkCountPadding: margins(2px, 0px, 3px, 1px); -starrefRecipientBg: lightButtonBgOver; -starrefRecipientBgDisabled: windowBgOver; starrefRecipientArrow: icon{{ "intro_country_dropdown", lightButtonFg }}; starrefCommissionFont: font(10px semibold); starrefCommissionPadding: margins(3px, 0px, 3px, 0px); diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 4cf3d7e74b..27623d8d61 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -74,7 +74,6 @@ menuIconUnmute: icon {{ "menu/unmute", menuIconColor }}; menuIconSchedule: icon {{ "menu/calendar", menuIconColor }}; menuIconReschedule: icon {{ "menu/reschedule", menuIconColor }}; menuIconSend: icon {{ "menu/send", menuIconColor }}; -menuIconFlip: icon {{ "menu/flip", menuIconColor }}; menuIconWhenOnline: icon {{ "menu/send_when_online", menuIconColor }}; menuIconPalette: icon {{ "menu/palette", menuIconColor }}; menuIconImportTheme: icon {{ "menu/import_theme", menuIconColor }}; @@ -107,7 +106,6 @@ menuIconTranslate: icon {{ "menu/translate", menuIconColor }}; menuIconReportAntiSpam: icon {{ "menu/false_positive", menuIconColor }}; menuIconSpoiler: icon {{ "menu/spoiler_on", menuIconColor }}; menuIconQualityHigh: icon {{ "menu/quality_hd", menuIconColor }}; -menuIconDisable: icon {{ "menu/disable", menuIconColor }}; menuIconPhotoSet: icon {{ "menu/photo_set", menuIconColor }}; menuIconPhotoSuggest: icon {{ "menu/photo_suggest", menuIconColor }}; menuIconNewWindow: icon {{ "menu/new_window", menuIconColor }}; @@ -173,8 +171,6 @@ menuIconWinHello: icon {{ "menu/passcode_winhello", menuIconColor }}; menuIconTouchID: icon {{ "menu/passcode_finger", menuIconColor }}; menuIconAppleWatch: icon {{ "menu/passcode_watch", menuIconColor }}; menuIconSystemPwd: menuIconPermissions; -menuIconPlayerFullScreen: icon {{ "player/player_fullscreen", menuIconColor }}; -menuIconPlayerWindowed: icon {{ "player/player_minimize", menuIconColor }}; menuIconStarRefShare: icon {{ "menu/stars_share", menuIconColor }}; menuIconStarRefLink: icon {{ "settings/premium/features/feature_links2", menuIconColor }}; menuIconTransparent: icon {{ "menu/affiliate_transparent", menuIconColor }}; @@ -201,7 +197,6 @@ menuIconStarsRefund: icon {{ "menu/auction_refund", menuIconColor }}; menuIconStarsCarryover: icon {{ "menu/auction_carry", menuIconColor }}; menuIconTon: icon{{ "payments/ton_emoji-18x18", menuIconColor, point(3px, 2px) }}; menuIconReorder: icon{{ "menu/reorder-24x24", menuIconColor }}; -menuIconTools: icon{{ "menu/craft_tools-24x24", menuIconColor }}; menuIconCraftTraits: icon{{ "menu/craft_random-24x24", menuIconColor }}; menuIconCraftChance: icon{{ "menu/craft_chance-24x24", menuIconColor }}; menuIconCraft: icon{{ "menu/craft_start-24x24", menuIconColor }}; @@ -221,7 +216,6 @@ menuIconMuteForAnyTextPosition: point(14px, 9px); menuIconMuteForAnyTextFont: font(8px semibold); menuBlueIconPhotoSet: icon {{ "menu/photo_set", lightButtonFg }}; -menuBlueIconPhotoSuggest: icon {{ "menu/photo_suggest", lightButtonFg }}; menuBlueIconPremium: icon{{ "menu/premium", lightButtonFg }}; menuBlueIconColorNames: icon{{ "settings/premium/features/feature_color_names", lightButtonFg }}; menuBlueIconWallpaper: icon{{ "settings/premium/features/feature_wallpaper", lightButtonFg }}; diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 4d6ee0ce7c..dce9e2cec0 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -83,7 +83,6 @@ notifySendReply: IconButton { notifyFadeRight: icon {{ "fade_horizontal", notificationBg }}; titleUnreadCounterTop: 6px; -titleUnreadCounterRight: 35px; mainMenuWidth: 274px; mainMenuCoverHeight: 134px; From af2b66bb5c4bf993bc4a5dda5c388064cf60dea4 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 8 Apr 2026 11:44:35 +0300 Subject: [PATCH 059/154] Migrated CI workflows to depot.dev runners for faster builds. --- .github/workflows/linux.yml | 2 +- .github/workflows/mac.yml | 2 +- .github/workflows/mac_packaged.yml | 2 +- .github/workflows/snap.yml | 2 +- .github/workflows/win.yml | 2 +- README.md | 14 ++++++++++++++ 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2f8091b621..6374ee4499 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -44,7 +44,7 @@ jobs: linux: name: Rocky Linux 8 - runs-on: ubuntu-latest + runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }} strategy: matrix: diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index c80ff4db98..02398ecdca 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -40,7 +40,7 @@ jobs: macos: name: MacOS - runs-on: macos-latest + runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-macos-latest' || 'macos-latest' }} strategy: matrix: diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 2e91e85491..ecdd2ad71e 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -40,7 +40,7 @@ jobs: macos: name: MacOS - runs-on: macos-latest + runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-macos-latest' || 'macos-latest' }} strategy: matrix: diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 8080e600f5..a72869bfb4 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -40,7 +40,7 @@ jobs: snap: name: Ubuntu - runs-on: ubuntu-latest + runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }} env: UPLOAD_ARTIFACT: "true" diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 0e36af68c5..36da5d64b6 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -44,7 +44,7 @@ jobs: windows: name: Windows - runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || 'windows-latest' }} + runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || ((github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-windows-latest-16' || 'windows-latest') }} strategy: matrix: diff --git a/README.md b/README.md index b5cabb358e..89c543e35f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This is the complete source code and the build instructions for the official [Te [![Build Status](https://github.com/telegramdesktop/tdesktop/workflows/Windows./badge.svg)](https://github.com/telegramdesktop/tdesktop/actions) [![Build Status](https://github.com/telegramdesktop/tdesktop/workflows/MacOS./badge.svg)](https://github.com/telegramdesktop/tdesktop/actions) [![Build Status](https://github.com/telegramdesktop/tdesktop/workflows/Linux./badge.svg)](https://github.com/telegramdesktop/tdesktop/actions) +[![Built with Depot](https://img.shields.io/badge/Built%20with-Depot.dev-46A75A)](https://depot.dev) [![Preview of Telegram Desktop][preview_image]][preview_image_url] @@ -83,3 +84,16 @@ Version **1.8.15** was the last that supports older systems [linux]: docs/building-linux.md [preview_image]: https://github.com/telegramdesktop/tdesktop/blob/dev/docs/assets/preview.png "Preview of Telegram Desktop" [preview_image_url]: https://raw.githubusercontent.com/telegramdesktop/tdesktop/dev/docs/assets/preview.png + +## Thanks to + + + + + + Depot + + + +CI infrastructure sponsored by [Depot](https://depot.dev) — fast GitHub Actions runners. + From 2ada258819cbe66c69dbfd2d5f4f649df2fd340f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 1 May 2026 01:06:50 +0000 Subject: [PATCH 060/154] Update User-Agent for DNS to Chrome 147.0.0.0. --- .../SourceFiles/mtproto/details/mtproto_domain_resolver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index 056d22aac6..3ef4c14ab1 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,7 +65,7 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/146.0.0.0 Safari/537.36"); + "Chrome/147.0.0.0 Safari/537.36"); return kResult; } From cb5ad29e8002bcccadeebe876602548c8d428a09 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 10:06:05 +0300 Subject: [PATCH 061/154] Exposed text selection methods on MediaGenericPart and text part. --- .../view/media/history_view_media_generic.cpp | 35 +++++++++++++++++-- .../view/media/history_view_media_generic.h | 14 ++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index 7f522d5a50..de7f3e3755 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -55,6 +55,21 @@ auto MediaGenericPart::stickerTakePlayer( return nullptr; } +uint16 MediaGenericPart::fullSelectionLength() const { + return 0; +} + +TextSelection MediaGenericPart::adjustSelection( + TextSelection selection, + TextSelectType type) const { + return selection; +} + +TextForMimeData MediaGenericPart::selectedText( + TextSelection selection) const { + return {}; +} + MediaGeneric::MediaGeneric( not_null parent, Fn data, const Lottie::ColorReplacements *replacements ) -> std::unique_ptr; + + [[nodiscard]] virtual uint16 fullSelectionLength() const; + [[nodiscard]] virtual TextSelection adjustSelection( + TextSelection selection, + TextSelectType type) const; + [[nodiscard]] virtual TextForMimeData selectedText( + TextSelection selection) const; }; struct MediaGenericDescriptor { @@ -158,6 +165,13 @@ public: StateRequest request, int outerWidth) const override; + [[nodiscard]] uint16 fullSelectionLength() const override; + [[nodiscard]] TextSelection adjustSelection( + TextSelection selection, + TextSelectType type) const override; + [[nodiscard]] TextForMimeData selectedText( + TextSelection selection) const override; + QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; From 4039101e0cad9e568da658148ad88e2fb6bd98c1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 10:09:52 +0300 Subject: [PATCH 062/154] Aggregated cross-part text selection across MediaGeneric parts. --- .../view/media/history_view_media_generic.cpp | 103 +++++++++++++++++- .../view/media/history_view_media_generic.h | 8 ++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index de7f3e3755..11f29cf47e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -152,12 +152,23 @@ void MediaGeneric::draw(Painter &p, const PaintContext &context) const { } } + const auto fullSelection = (context.selection == FullSelection); auto translated = 0; + auto symbolOffset = uint16(0); for (const auto &entry : _entries) { const auto raw = entry.object.get(); const auto height = raw->height(); - raw->draw(p, this, context, outer); + const auto length = raw->fullSelectionLength(); + if (length > 0 && !fullSelection) { + const auto local = UnshiftItemSelection( + context.selection, + symbolOffset); + raw->draw(p, this, context.withSelection(local), outer); + } else { + raw->draw(p, this, context, outer); + } translated += height; + symbolOffset = uint16(symbolOffset + length); p.translate(0, height); } p.translate(0, -translated); @@ -178,16 +189,29 @@ TextState MediaGeneric::textState( return result; } + auto symbolOffset = uint16(0); for (const auto &entry : _entries) { const auto raw = entry.object.get(); const auto height = raw->height(); + const auto length = raw->fullSelectionLength(); if (point.y() >= 0 && point.y() < height) { const auto part = raw->textState(point, request, outer); result.link = part.link; + result.cursor = part.cursor; + if (length > 0) { + result.symbol = uint16(symbolOffset + part.symbol); + result.afterSymbol = part.afterSymbol; + result.overMessageText + = (part.cursor == CursorState::Text); + } else { + result.symbol = symbolOffset; + } return result; } point.setY(point.y() - height); + symbolOffset = uint16(symbolOffset + length); } + result.symbol = symbolOffset; return result; } @@ -204,6 +228,83 @@ void MediaGeneric::clickHandlerPressedChanged( } } +bool MediaGeneric::hasTextForCopy() const { + return fullSelectionLength() > 0; +} + +uint16 MediaGeneric::fullSelectionLength() const { + auto total = uint16(0); + for (const auto &entry : _entries) { + total = uint16(total + entry.object->fullSelectionLength()); + } + return total; +} + +TextForMimeData MediaGeneric::selectedText(TextSelection selection) const { + auto offset = uint16(0); + auto result = TextForMimeData(); + for (const auto &entry : _entries) { + const auto length = entry.object->fullSelectionLength(); + if (length > 0) { + auto part = entry.object->selectedText( + UnshiftItemSelection(selection, offset)); + if (!part.empty()) { + if (result.empty()) { + result = std::move(part); + } else { + result.append('\n').append(std::move(part)); + } + } + } + offset = uint16(offset + length); + } + return result; +} + +TextSelection MediaGeneric::adjustSelection( + TextSelection selection, + TextSelectType type) const { + if (selection == FullSelection) { + return selection; + } + auto offset = uint16(0); + auto firstFrom = std::optional(); + auto firstOffset = uint16(0); + auto lastTo = uint16(0); + auto lastOffset = uint16(0); + for (const auto &entry : _entries) { + const auto length = entry.object->fullSelectionLength(); + if (length > 0) { + const auto end = uint16(offset + length); + if (selection.from < end && selection.to > offset) { + const auto from = uint16((selection.from > offset) + ? (selection.from - offset) + : 0); + const auto to = uint16((selection.to < end) + ? (selection.to - offset) + : length); + const auto local = entry.object->adjustSelection( + { from, to }, + type); + if (!firstFrom.has_value()) { + firstFrom = local.from; + firstOffset = offset; + } + lastTo = local.to; + lastOffset = offset; + } + } + offset = uint16(offset + length); + } + if (!firstFrom.has_value()) { + return selection; + } + return { + uint16(firstOffset + *firstFrom), + uint16(lastOffset + lastTo), + }; +} + std::unique_ptr MediaGeneric::stickerTakePlayer( not_null data, const Lottie::ColorReplacements *replacements) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index 42e4f1a725..3f7aa7e7a1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -92,6 +92,14 @@ public: void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; + [[nodiscard]] bool hasTextForCopy() const override; + [[nodiscard]] TextForMimeData selectedText( + TextSelection selection) const override; + [[nodiscard]] TextSelection adjustSelection( + TextSelection selection, + TextSelectType type) const override; + [[nodiscard]] uint16 fullSelectionLength() const override; + void clickHandlerActiveChanged( const ClickHandlerPtr &p, bool active) override; From 1131f729d584c6dcd1002b9c1cd0aa92fcc2de92 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 11:04:23 +0300 Subject: [PATCH 063/154] [poll-view] Replaced scattered footer layout logic with single layout. --- .../history/view/media/history_view_poll.cpp | 535 ++++++++---------- 1 file changed, 241 insertions(+), 294 deletions(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 5af0bbb852..67aa4bd9c5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -483,58 +483,166 @@ struct Poll::Footer : public Poll::Part { mutable base::Timer _closeTimer; private: - [[nodiscard]] int topSkip() const; - [[nodiscard]] int textTop() const; - [[nodiscard]] bool hasTimerLine(int innerWidth) const; + struct Layout { + enum class Kind { + PassiveLabel, + SaveOption, + AdminVotes, + AdminBack, + LinkButton, + }; + Kind kind = Kind::PassiveLabel; + ClickHandlerPtr link; + bool compact = false; + bool timerFolded = false; + bool timerSeparate = false; + int topSkip = 0; + int textY = 0; + int timerY = 0; + int totalHeight = 0; + }; + [[nodiscard]] Layout computeLayout(int innerWidth) const; [[nodiscard]] bool hasCloseDate() const; [[nodiscard]] QString closeTimerText() const; - [[nodiscard]] QRect timerRect(int left, int innerWidth) const; + [[nodiscard]] QRect timerRect( + const Layout &layout, + int left, + int innerWidth) const; [[nodiscard]] bool timerFooterMultiline(int paintw) const; [[nodiscard]] bool centeredOverlapsInfo( int textWidth, int innerWidth) const; - [[nodiscard]] int bottomLineWidth(int innerWidth) const; - [[nodiscard]] int dateInfoPadding(int innerWidth) const; + [[nodiscard]] QString linkButtonText() const; void toggleLinkRipple(bool pressed); }; -int Poll::Footer::topSkip() const { - return _owner->canAddOption() - ? 0 - : st::historyPollTotalVotesSkip; -} - -int Poll::Footer::textTop() const { - return topSkip() - + st::msgPadding.bottom() - + st::historyPollBottomButtonTop; -} - bool Poll::Footer::hasCloseDate() const { return _owner->_poll->closeDate > 0 && !(_owner->_flags & PollData::Flag::Closed); } -bool Poll::Footer::hasTimerLine(int innerWidth) const { - if (_owner->inlineFooter() || _owner->showVotersCount()) { - return timerFooterMultiline(innerWidth); +auto Poll::Footer::computeLayout(int innerWidth) const -> Layout { + Layout result; + result.compact = _owner->inlineFooter(); + result.topSkip = _owner->canAddOption() + ? 0 + : st::historyPollTotalVotesSkip; + + const auto timerText = closeTimerText(); + const auto hasTimer = !timerText.isEmpty(); + const auto lineHeight = st::msgDateFont->height; + + if (result.compact) { + result.kind = Layout::Kind::PassiveLabel; + if (hasTimer) { + if (timerFooterMultiline(innerWidth)) { + result.timerSeparate = true; + } else { + result.timerFolded = true; + } + } + } else if (_owner->_addOptionActive) { + result.kind = Layout::Kind::SaveOption; + result.link = _saveOptionLink; + } else if (_owner->isAuthorNotVoted() + && !_owner->_adminShowResults + && !_owner->canSendVotes()) { + if (_owner->_totalVotes > 0) { + result.kind = Layout::Kind::AdminVotes; + result.link = _adminVotesLink; + } else { + result.kind = Layout::Kind::PassiveLabel; + } + } else if (_owner->_adminShowResults + && _owner->isAuthorNotVoted()) { + result.kind = Layout::Kind::AdminBack; + result.link = _adminBackVoteLink; + } else if (_owner->showVotersCount()) { + result.kind = Layout::Kind::PassiveLabel; + if (hasTimer) { + if (timerFooterMultiline(innerWidth)) { + result.timerSeparate = true; + } else { + result.timerFolded = true; + } + } + } else { + result.kind = Layout::Kind::LinkButton; + const auto votedPublic = _owner->_voted + && (_owner->_flags & PollData::Flag::PublicVotes); + result.link = (_owner->showVotes() || votedPublic) + ? _showResultsLink + : _owner->canSendVotes() + ? _sendVotesLink + : nullptr; + if (hasTimer) { + result.timerSeparate = true; + } } - return hasCloseDate(); + + const auto buttonSkip = result.compact + ? 0 + : st::historyPollBottomButtonSkip; + result.textY = result.compact + ? st::msgPadding.bottom() + : (result.topSkip + + st::msgPadding.bottom() + + st::historyPollBottomButtonTop); + result.timerY = result.textY + lineHeight; + + auto bottomW = 0; + if (result.timerSeparate) { + bottomW = st::msgDateFont->width(timerText); + } else if (!result.timerFolded) { + switch (result.kind) { + case Layout::Kind::PassiveLabel: + bottomW = _totalVotesLabel.maxWidth(); + break; + case Layout::Kind::SaveOption: + bottomW = st::semiboldFont->width( + tr::lng_polls_add_option_save(tr::now)); + break; + case Layout::Kind::AdminVotes: + bottomW = _adminVotesLabel.maxWidth(); + break; + case Layout::Kind::AdminBack: + bottomW = _adminBackVoteLabel.maxWidth(); + break; + case Layout::Kind::LinkButton: + bottomW = st::semiboldFont->width(linkButtonText()); + break; + } + } + const auto dateInfoPad = (bottomW > 0 + && centeredOverlapsInfo(bottomW, innerWidth)) + ? lineHeight + : 0; + + result.totalHeight = result.topSkip + + buttonSkip + + lineHeight + + (result.timerSeparate ? lineHeight : 0) + + dateInfoPad + + st::msgPadding.bottom(); + + return result; +} + +QString Poll::Footer::linkButtonText() const { + const auto votedPublic = _owner->_voted + && (_owner->_flags & PollData::Flag::PublicVotes); + return (_owner->showVotes() || votedPublic) + ? ((_owner->_flags & PollData::Flag::PublicVotes) + ? tr::lng_polls_view_votes( + tr::now, + lt_count, + _owner->_totalVotes) + : tr::lng_polls_view_results(tr::now)) + : tr::lng_polls_submit_votes(tr::now); } int Poll::Footer::countHeight(int innerWidth) const { - const auto inline_ = _owner->inlineFooter(); - const auto top = topSkip(); - const auto buttonSkip = inline_ - ? 0 - : st::historyPollBottomButtonSkip; - const auto timerLine = hasTimerLine(innerWidth); - return top - + buttonSkip - + st::msgDateFont->height - + (timerLine ? st::msgDateFont->height : 0) - + dateInfoPadding(innerWidth) - + st::msgPadding.bottom(); + return computeLayout(innerWidth).totalHeight; } void Poll::Footer::draw( @@ -543,65 +651,19 @@ void Poll::Footer::draw( int innerWidth, int outerWidth, const PaintContext &context) const { + const auto layout = computeLayout(innerWidth); const auto stm = context.messageStyle(); - const auto inline_ = _owner->inlineFooter(); - if (inline_) { - const auto top = st::msgPadding.bottom(); - p.setPen(stm->msgDateFg); - const auto timerText = closeTimerText(); - if (timerText.isEmpty()) { - const auto labelWidth = _totalVotesLabel.maxWidth(); - const auto labelLeft = left + (innerWidth - labelWidth) / 2; - _totalVotesLabel.drawLeftElided( - p, - labelLeft, - top, - labelWidth, - outerWidth); - } else if (timerFooterMultiline(innerWidth)) { - const auto labelWidth = _totalVotesLabel.maxWidth(); - const auto labelLeft = left + (innerWidth - labelWidth) / 2; - _totalVotesLabel.drawLeftElided( - p, - labelLeft, - top, - labelWidth, - outerWidth); - p.setFont(st::msgDateFont); - const auto rect = timerRect(left, innerWidth); - p.drawTextLeft( - rect.x(), - rect.y(), - outerWidth, - timerText, - rect.width()); - } else { - p.setFont(st::msgDateFont); - const auto sep = QString::fromUtf8(" \xC2\xB7 "); - const auto full = _totalVotesLabel.toString() - + sep - + timerText; - const auto fullw = st::msgDateFont->width(full); - p.drawTextLeft( - left + (innerWidth - fullw) / 2, - top, - outerWidth, - full, - fullw); - } - return; + if (!layout.link) { + _linkRipple.reset(); } - const auto stringtop = textTop(); - - if (_linkRipple) { - const auto rippleTop = topSkip(); + if (_linkRipple && !layout.compact) { p.setOpacity(st::historyPollRippleOpacity); _linkRipple->paint( p, left - st::msgPadding.left() - _linkRippleShift, - rippleTop, + layout.topSkip, outerWidth, &stm->msgWaveformInactive->c); if (_linkRipple->empty()) { @@ -609,131 +671,99 @@ void Poll::Footer::draw( } p.setOpacity(1.); } - if (_owner->_addOptionActive) { + + switch (layout.kind) { + case Layout::Kind::PassiveLabel: { + p.setPen(stm->msgDateFg); + if (layout.timerFolded) { + p.setFont(st::msgDateFont); + const auto sep = QString::fromUtf8(" \xC2\xB7 "); + const auto full = _totalVotesLabel.toString() + + sep + + closeTimerText(); + const auto fullw = st::msgDateFont->width(full); + p.drawTextLeft( + left + (innerWidth - fullw) / 2, + layout.textY, + outerWidth, + full, + fullw); + } else { + const auto labelWidth = _totalVotesLabel.maxWidth(); + const auto labelLeft = left + + (innerWidth - labelWidth) / 2; + _totalVotesLabel.drawLeftElided( + p, + labelLeft, + layout.textY, + labelWidth, + outerWidth); + } + break; + } + case Layout::Kind::SaveOption: { p.setFont(st::semiboldFont); p.setPen(stm->msgFileThumbLinkFg); const auto text = tr::lng_polls_add_option_save(tr::now); const auto textw = st::semiboldFont->width(text); p.drawTextLeft( left + (innerWidth - textw) / 2, - stringtop, + layout.textY, outerWidth, text, textw); - return; + break; } - if (_owner->isAuthorNotVoted() - && !_owner->_adminShowResults - && !_owner->canSendVotes()) { - if (_owner->_totalVotes > 0) { - p.setPen(stm->msgFileThumbLinkFg); - const auto labelWidth = _adminVotesLabel.maxWidth(); - _adminVotesLabel.drawLeft( - p, - left + (innerWidth - labelWidth) / 2, - stringtop, - labelWidth, - outerWidth); - } else { - p.setPen(stm->msgDateFg); - const auto textw = _totalVotesLabel.maxWidth(); - _totalVotesLabel.drawLeft( - p, - left + (innerWidth - textw) / 2, - stringtop, - textw, - outerWidth); - } - } else if (_owner->_adminShowResults && _owner->isAuthorNotVoted()) { + case Layout::Kind::AdminVotes: { + p.setPen(stm->msgFileThumbLinkFg); + const auto labelWidth = _adminVotesLabel.maxWidth(); + _adminVotesLabel.drawLeft( + p, + left + (innerWidth - labelWidth) / 2, + layout.textY, + labelWidth, + outerWidth); + break; + } + case Layout::Kind::AdminBack: { p.setPen(stm->msgFileThumbLinkFg); const auto backw = _adminBackVoteLabel.maxWidth(); _adminBackVoteLabel.drawLeft( p, left + (innerWidth - backw) / 2, - stringtop, + layout.textY, backw, outerWidth); - } else if (_owner->showVotersCount()) { - _linkRipple.reset(); - p.setPen(stm->msgDateFg); - const auto timerText = closeTimerText(); - if (timerText.isEmpty()) { - const auto labelWidth = _totalVotesLabel.maxWidth(); - const auto labelLeft = left + (innerWidth - labelWidth) / 2; - _totalVotesLabel.draw( - p, - labelLeft, - stringtop, - labelWidth, - style::al_top); - } else if (timerFooterMultiline(innerWidth)) { - const auto labelWidth = _totalVotesLabel.maxWidth(); - const auto labelLeft = left + (innerWidth - labelWidth) / 2; - _totalVotesLabel.draw( - p, - labelLeft, - stringtop, - labelWidth, - style::al_top); - p.setFont(st::msgDateFont); - const auto rect = timerRect(left, innerWidth); - p.drawTextLeft( - rect.x(), - rect.y(), - outerWidth, - timerText, - rect.width()); - } else { - p.setFont(st::msgDateFont); - const auto sep = QString::fromUtf8(" \xC2\xB7 "); - const auto full = _totalVotesLabel.toString() - + sep - + timerText; - const auto fullw = st::msgDateFont->width(full); - p.drawTextLeft( - left + (innerWidth - fullw) / 2, - stringtop, - outerWidth, - full, - fullw); - } - } else { - const auto votedPublic = _owner->_voted - && (_owner->_flags & PollData::Flag::PublicVotes); - const auto link = (_owner->showVotes() || votedPublic) - ? _showResultsLink - : _owner->canSendVotes() - ? _sendVotesLink - : nullptr; + break; + } + case Layout::Kind::LinkButton: { p.setFont(st::semiboldFont); - p.setPen(link ? stm->msgFileThumbLinkFg : stm->msgDateFg); - const auto string = (_owner->showVotes() || votedPublic) - ? ((_owner->_flags & PollData::Flag::PublicVotes) - ? tr::lng_polls_view_votes( - tr::now, - lt_count, - _owner->_totalVotes) - : tr::lng_polls_view_results(tr::now)) - : tr::lng_polls_submit_votes(tr::now); + p.setPen(layout.link + ? stm->msgFileThumbLinkFg + : stm->msgDateFg); + const auto string = linkButtonText(); const auto stringw = st::semiboldFont->width(string); p.drawTextLeft( left + (innerWidth - stringw) / 2, - stringtop, + layout.textY, outerWidth, string, stringw); + break; + } + } + + if (layout.timerSeparate) { + p.setFont(st::msgDateFont); + p.setPen(stm->msgDateFg); const auto timerText = closeTimerText(); - if (!timerText.isEmpty()) { - p.setFont(st::msgDateFont); - p.setPen(stm->msgDateFg); - const auto rect = timerRect(left, innerWidth); - p.drawTextLeft( - rect.x(), - rect.y(), - outerWidth, - timerText, - rect.width()); - } + const auto timerw = st::msgDateFont->width(timerText); + p.drawTextLeft( + left + (innerWidth - timerw) / 2, + layout.timerY, + outerWidth, + timerText, + timerw); } } @@ -743,8 +773,10 @@ TextState Poll::Footer::textState( int innerWidth, int outerWidth, StateRequest request) const { + const auto layout = computeLayout(innerWidth); TextState result; - const auto timer = timerRect(left, innerWidth); + + const auto timer = timerRect(layout, left, innerWidth); if (!timer.isEmpty() && timer.contains(point)) { result.customTooltip = true; using Flag = Ui::Text::StateRequest::Flag; @@ -753,34 +785,15 @@ TextState Poll::Footer::textState( base::unixtime::parse(_owner->_poll->closeDate)); } } - if (_owner->inlineFooter()) { + if (layout.compact) { return result; } - const auto top = topSkip(); - const auto h = countHeight(innerWidth); - if (point.y() < top || point.y() >= h) { + if (point.y() < layout.topSkip + || point.y() >= layout.totalHeight) { return result; } _owner->_lastLinkPoint = point; - if (_owner->_addOptionActive) { - result.link = _saveOptionLink; - } else if (_owner->isAuthorNotVoted() - && !_owner->_adminShowResults - && !_owner->canSendVotes()) { - if (_owner->_totalVotes > 0) { - result.link = _adminVotesLink; - } - } else if (_owner->_adminShowResults && _owner->isAuthorNotVoted()) { - result.link = _adminBackVoteLink; - } else if (!_owner->showVotersCount()) { - const auto votedPublic = _owner->_voted - && (_owner->_flags & PollData::Flag::PublicVotes); - result.link = (_owner->showVotes() || votedPublic) - ? _showResultsLink - : _owner->canSendVotes() - ? _sendVotesLink - : nullptr; - } + result.link = layout.link; return result; } @@ -799,10 +812,12 @@ void Poll::Footer::clickHandlerPressedChanged( void Poll::Footer::toggleLinkRipple(bool pressed) { if (pressed) { const auto outerWidth = _owner->width(); - const auto h = countHeight( - outerWidth - st::msgPadding.left() - st::msgPadding.right()); - const auto rippleTop = topSkip(); - const auto linkHeight = h - rippleTop; + const auto innerWidth = outerWidth + - st::msgPadding.left() + - st::msgPadding.right(); + const auto layout = computeLayout(innerWidth); + const auto rippleTop = layout.topSkip; + const auto linkHeight = layout.totalHeight - rippleTop; if (!_linkRipple) { auto mask = _owner->isRoundedInBubbleBottom() ? static_cast(_owner->_parent.get()) @@ -4003,46 +4018,34 @@ QString Poll::Footer::closeTimerText() const { : tr::lng_polls_ends_in_time(tr::now, lt_time, timer); } -QRect Poll::Footer::timerRect(int left, int innerWidth) const { +QRect Poll::Footer::timerRect( + const Layout &layout, + int left, + int innerWidth) const { const auto timerText = closeTimerText(); if (timerText.isEmpty()) { return {}; } const auto lineHeight = st::msgDateFont->height; const auto timerw = st::msgDateFont->width(timerText); - const auto inline_ = _owner->inlineFooter(); - if (inline_ || _owner->showVotersCount()) { - const auto y = inline_ ? st::msgPadding.bottom() : textTop(); - if (timerFooterMultiline(innerWidth)) { - return QRect( - left + (innerWidth - timerw) / 2, - y + lineHeight, - timerw, - lineHeight); - } + if (layout.timerSeparate) { + return QRect( + left + (innerWidth - timerw) / 2, + layout.timerY, + timerw, + lineHeight); + } else if (layout.timerFolded) { const auto sep = QString::fromUtf8(" \xC2\xB7 "); const auto label = _totalVotesLabel.toString(); const auto prefixw = st::msgDateFont->width(label + sep); const auto fullw = prefixw + timerw; return QRect( left + (innerWidth - fullw) / 2 + prefixw, - y, + layout.textY, timerw, lineHeight); } - const auto suppressedByLink = _owner->_addOptionActive - || (_owner->isAuthorNotVoted() - && !_owner->_adminShowResults - && !_owner->canSendVotes()) - || (_owner->_adminShowResults && _owner->isAuthorNotVoted()); - if (suppressedByLink) { - return {}; - } - return QRect( - left + (innerWidth - timerw) / 2, - textTop() + st::semiboldFont->height, - timerw, - lineHeight); + return {}; } bool Poll::Footer::timerFooterMultiline(int paintw) const { @@ -4067,62 +4070,6 @@ bool Poll::Footer::centeredOverlapsInfo( return (innerWidth + textWidth) / 2 > innerWidth - skipw; } -int Poll::Footer::bottomLineWidth(int innerWidth) const { - const auto inline_ = _owner->inlineFooter(); - const auto timerText = closeTimerText(); - const auto timerLine = hasTimerLine(innerWidth); - - if (inline_ || _owner->showVotersCount()) { - if (timerText.isEmpty()) { - return _totalVotesLabel.maxWidth(); - } else if (timerLine) { - return st::msgDateFont->width(timerText); - } - // Single-line timer — timerFooterMultiline already handles. - return 0; - } - - if (_owner->_addOptionActive) { - return timerLine - ? 0 - : st::semiboldFont->width( - tr::lng_polls_add_option_save(tr::now)); - } else if (_owner->isAuthorNotVoted() - && !_owner->_adminShowResults - && !_owner->canSendVotes()) { - return timerLine - ? 0 - : (_owner->_totalVotes > 0) - ? _adminVotesLabel.maxWidth() - : _totalVotesLabel.maxWidth(); - } else if (_owner->_adminShowResults - && _owner->isAuthorNotVoted()) { - return timerLine ? 0 : _adminBackVoteLabel.maxWidth(); - } - - if (timerLine) { - return st::msgDateFont->width(timerText); - } - const auto votedPublic = _owner->_voted - && (_owner->_flags & PollData::Flag::PublicVotes); - const auto string = (_owner->showVotes() || votedPublic) - ? ((_owner->_flags & PollData::Flag::PublicVotes) - ? tr::lng_polls_view_votes( - tr::now, - lt_count, - _owner->_totalVotes) - : tr::lng_polls_view_results(tr::now)) - : tr::lng_polls_submit_votes(tr::now); - return st::semiboldFont->width(string); -} - -int Poll::Footer::dateInfoPadding(int innerWidth) const { - const auto w = bottomLineWidth(innerWidth); - return (w > 0 && centeredOverlapsInfo(w, innerWidth)) - ? st::msgDateFont->height - : 0; -} - Poll::~Poll() { history()->owner().unregisterPollView(_poll, _parent); if (hasHeavyPart()) { From d5e8bd767f3e284491fc9bb838b8697b27b98d41 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 11:13:20 +0300 Subject: [PATCH 064/154] [poll-view] Added missing resize request on admin results toggle. --- Telegram/SourceFiles/history/view/media/history_view_poll.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 67aa4bd9c5..8cb040658c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -1814,6 +1814,7 @@ Poll::Footer::Footer(not_null owner) owner->_adminShowResults = true; owner->_optionsPart->updateAnswerVotes(); owner->_optionsPart->startAnswersAnimation(); + owner->history()->owner().requestViewResize(owner->_parent); } }))) , _adminBackVoteLink( @@ -1824,6 +1825,7 @@ Poll::Footer::Footer(not_null owner) owner->_adminShowResults = false; owner->_optionsPart->updateAnswerVotes(); owner->_optionsPart->startAnswersAnimation(); + owner->history()->owner().requestViewResize(owner->_parent); }))) , _saveOptionLink( std::make_shared(crl::guard( From 1d4cecc66d52a8b4ffe4fd543929ccb60fe1649d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 11:32:22 +0300 Subject: [PATCH 065/154] Fixed poll checkmark persisting after vote retraction. --- Telegram/SourceFiles/api/api_polls.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index a1f68a4110..09dcd0a064 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -164,6 +164,12 @@ void Polls::sendVotes( if (showSending) { poll->sendingVotes = options; _session->data().requestItemRepaint(item); + } else if (poll && options.empty() && poll->voted()) { + for (auto &answer : poll->answers) { + answer.chosen = false; + } + ++poll->version; + _session->data().notifyPollUpdateDelayed(poll); } auto prepared = QVector(); From b00c36c389b86f1ae4b3b4488f262973f9bc6b58 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 11:43:32 +0300 Subject: [PATCH 066/154] [poll-view] Fixed hidden results leak in forwarded polls to channels. --- Telegram/SourceFiles/history/view/media/history_view_poll.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 8cb040658c..1a93fa2a3d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -1893,7 +1893,7 @@ bool Poll::showVotes() const { return true; } if (_flags & PollData::Flag::HideResultsUntilClose) { - return (_flags & PollData::Flag::Closed) || _parent->data()->out(); + return (_flags & PollData::Flag::Closed) || _poll->creator(); } return _voted || (_flags & PollData::Flag::Closed); } From 9ea03156a1ccb79506a7d0d30a39d77e9ac45a1b Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 2 May 2026 14:34:17 +0300 Subject: [PATCH 067/154] Replaced msvc-dev-cmd action with the Node 24 Eden-CI fork. --- .github/workflows/win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 36da5d64b6..fa531fb256 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -101,7 +101,7 @@ jobs: git config --global user.email "you@example.com" git config --global user.name "Sample" - - uses: ilammy/msvc-dev-cmd@v1.13.0 + - uses: Eden-CI/msvc-dev-cmd@master name: Native Tools Command Prompt. with: arch: ${{ matrix.arch }} From ec3e628b96262a98f12307fdfd0867e6e4bac3c4 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 3 May 2026 07:59:41 +0300 Subject: [PATCH 068/154] Removed redundant release trigger from changelog Pages workflow. --- .github/workflows/changelog.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 294f1ad3ef..1746d0f0fe 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -4,8 +4,6 @@ on: push: branches: [dev] paths: [changelog.txt] - release: - types: [published] workflow_dispatch: permissions: From 6931772561b5f6a37a68097cb23a54bb511c611e Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 1 May 2026 13:00:37 +0700 Subject: [PATCH 069/154] Replace draft even without common prefix. --- Telegram/SourceFiles/history/history_streamed_drafts.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/history_streamed_drafts.cpp b/Telegram/SourceFiles/history/history_streamed_drafts.cpp index 91e46265fc..1634fddb58 100644 --- a/Telegram/SourceFiles/history/history_streamed_drafts.cpp +++ b/Telegram/SourceFiles/history/history_streamed_drafts.cpp @@ -181,7 +181,7 @@ HistoryItem *HistoryStreamedDrafts::adoptIncoming( } const auto incomingText = qs(data.vmessage()); auto best = end(_drafts); - auto bestPrefix = 0; + auto bestPrefix = -1; for (auto i = begin(_drafts); i != end(_drafts); ++i) { const auto &draft = i->second; if (draft.rootId != rootId) { @@ -198,7 +198,7 @@ HistoryItem *HistoryStreamedDrafts::adoptIncoming( best = i; } } - if (best == end(_drafts) || bestPrefix <= 0) { + if (best == end(_drafts)) { return nullptr; } const auto item = best->second.message.get(); From efea01e03ea92860e57200ddc018f5ff01dc3b4f Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 1 May 2026 14:05:18 +0700 Subject: [PATCH 070/154] Improve send sticker from set box logic. --- .../info/settings/info_settings_widget.cpp | 7 +-- Telegram/SourceFiles/menu/menu_send.h | 40 +------------- Telegram/SourceFiles/menu/menu_send_details.h | 53 +++++++++++++++++++ .../business/settings_shortcut_messages.cpp | 6 --- .../SourceFiles/settings/settings_common.cpp | 5 ++ .../SourceFiles/settings/settings_common.h | 9 +--- Telegram/cmake/td_ui.cmake | 1 + 7 files changed, 62 insertions(+), 59 deletions(-) create mode 100644 Telegram/SourceFiles/menu/menu_send_details.h diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 0cb5aa3161..fc5689aede 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -189,12 +189,7 @@ void Widget::saveChanges(FnMut done) { } SendMenu::Details Widget::sendMenuDetails() const { - if (const auto provider - = dynamic_cast( - _inner.get())) { - return provider->sendMenuDetails(); - } - return ContentWidget::sendMenuDetails(); + return _inner->sendMenuDetails(); } bool Widget::processChosenSticker(ChatHelpers::FileChosen &&chosen) { diff --git a/Telegram/SourceFiles/menu/menu_send.h b/Telegram/SourceFiles/menu/menu_send.h index d748a57523..81d107fe37 100644 --- a/Telegram/SourceFiles/menu/menu_send.h +++ b/Telegram/SourceFiles/menu/menu_send.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "api/api_common.h" +#include "menu/menu_send_details.h" namespace style { struct ComposeIcons; @@ -30,45 +31,6 @@ class Thread; namespace SendMenu { -enum class Type : uchar { - Disabled, - SilentOnly, - Scheduled, - ScheduledToUser, // For "Send when online". - Reminder, - EditCommentPrice, -}; - -enum class SpoilerState : uchar { - None, - Enabled, - Possible, -}; - -enum class CaptionState : uchar { - None, - Below, - Above, -}; - -enum class PhotoQualityState : uchar { - None, - Standard, - High, -}; - -struct Details { - Type type = Type::Disabled; - SpoilerState spoiler = SpoilerState::None; - CaptionState caption = CaptionState::None; - PhotoQualityState photoQuality = PhotoQualityState::None; - TextWithTags commentPreview; - QString commentStreamerName; - std::optional price; - std::optional commentPriceMin; - bool effectAllowed = false; -}; - enum class FillMenuResult : uchar { Prepared, Skipped, diff --git a/Telegram/SourceFiles/menu/menu_send_details.h b/Telegram/SourceFiles/menu/menu_send_details.h new file mode 100644 index 0000000000..e30c7c2b05 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_send_details.h @@ -0,0 +1,53 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/text/text_entity.h" + +namespace SendMenu { + +enum class Type : uchar { + Disabled, + SilentOnly, + Scheduled, + ScheduledToUser, // For "Send when online". + Reminder, + EditCommentPrice, +}; + +enum class SpoilerState : uchar { + None, + Enabled, + Possible, +}; + +enum class CaptionState : uchar { + None, + Below, + Above, +}; + +enum class PhotoQualityState : uchar { + None, + Standard, + High, +}; + +struct Details { + Type type = Type::Disabled; + SpoilerState spoiler = SpoilerState::None; + CaptionState caption = CaptionState::None; + PhotoQualityState photoQuality = PhotoQualityState::None; + TextWithTags commentPreview; + QString commentStreamerName; + std::optional price; + std::optional commentPriceMin; + bool effectAllowed = false; +}; + +} // namespace SendMenu diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 5e1ba123b0..2c5aef1083 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -74,7 +74,6 @@ using namespace HistoryView; class ShortcutMessages : public AbstractSection - , public SendMenuDetailsProvider , private WindowListDelegate , private CornerButtonsDelegate { public: @@ -94,7 +93,6 @@ public: [[nodiscard]] rpl::producer title() override; [[nodiscard]] rpl::producer<> sectionShowBack() override; - [[nodiscard]] SendMenu::Details sendMenuDetails() const override; bool processChosenSticker(ChatHelpers::FileChosen &&chosen) override; void setInnerFocus() override; @@ -1450,10 +1448,6 @@ void ShortcutMessages::finishSending() { showAtEnd(); } -SendMenu::Details ShortcutMessages::sendMenuDetails() const { - return {}; -} - bool ShortcutMessages::processChosenSticker(ChatHelpers::FileChosen &&chosen) { if (!_composeControls) { return false; diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index e1855d81b1..866acc33f1 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "lottie/lottie_icon.h" +#include "menu/menu_send_details.h" #include "ui/effects/animations.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" @@ -296,6 +297,10 @@ AbstractSection::AbstractSection( : _controller(controller) { } +SendMenu::Details AbstractSection::sendMenuDetails() const { + return {}; +} + bool AbstractSection::processChosenSticker(ChatHelpers::FileChosen &&) { return false; } diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 9a62768575..3999fe9343 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -134,6 +134,7 @@ public: virtual void sectionSaveChanges(FnMut done) { done(); } + virtual SendMenu::Details sendMenuDetails() const; virtual bool processChosenSticker(ChatHelpers::FileChosen &&chosen); virtual void showFinished() { _showFinished.fire({}); @@ -204,14 +205,6 @@ private: }; -class SendMenuDetailsProvider { -public: - [[nodiscard]] virtual SendMenu::Details sendMenuDetails() const = 0; - - virtual ~SendMenuDetailsProvider() = default; - -}; - enum class IconType { Rounded, Round, diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 6fdf4ec5f1..4bbff50520 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -231,6 +231,7 @@ PRIVATE menu/menu_check_item.h menu/menu_item_rate_transcribe.cpp menu/menu_item_rate_transcribe.h + menu/menu_send_details.h menu/menu_timecode_action.cpp menu/menu_timecode_action.h menu/menu_ttl.cpp From 98a16c63eb389000614f791e58c1cc766419c765 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 1 May 2026 14:42:19 +0700 Subject: [PATCH 071/154] Fix bad OpenGL player colors on macOS. --- .../media/view/media_view_overlay_opengl.cpp | 16 +++++++++++++--- .../media/view/media_view_overlay_opengl.h | 3 ++- .../media/view/media_view_pip_opengl.cpp | 13 ++++++++++--- .../media/view/media_view_pip_opengl.h | 3 ++- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 0c796a88f6..0607feadba 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -274,6 +274,16 @@ void OverlayWidget::RendererGL::init(QOpenGLFunctions &f) { void OverlayWidget::RendererGL::deinit(QOpenGLFunctions *f) { _textures.destroy(f); + for (auto i = 0; i != 3; ++i) { + _rgbaSize[i] = QSize(); + _cacheKeys[i] = 0; + } + _lumaSize = QSize(); + _chromaSize = QSize(); + _chromaSizeV = QSize(); + _chromaNV12 = false; + _trackFrameIndex = 0; + _streamedIndex = 0; _imageProgram = std::nullopt; _texturedVertexShader = nullptr; _withTransparencyProgram = std::nullopt; @@ -424,8 +434,8 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( nv12changed ? QSize() : _chromaSize, yuv->u.stride / (nv12 ? 2 : 1), yuv->u.data); + _chromaSize = yuv->chromaSize; if (nv12) { - _chromaSize = yuv->chromaSize; _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4); } _chromaNV12 = nv12; @@ -443,10 +453,10 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( GL_ALPHA, GL_ALPHA, yuv->chromaSize, - _chromaSize, + _chromaSizeV, yuv->v.stride, yuv->v.data); - _chromaSize = yuv->chromaSize; + _chromaSizeV = yuv->chromaSize; _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index 4f4f5832c3..680e4448c4 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -135,7 +135,8 @@ private: Ui::GL::Textures<6> _textures; // image, sibling, right sibling, y, u, v QSize _rgbaSize[3]; QSize _lumaSize; - QSize _chromaSize; + QSize _chromaSize; // size of texture 4 (UV for NV12, U for YUV420) + QSize _chromaSizeV; // size of texture 5 (V for YUV420 only) qint64 _cacheKeys[3] = { 0 }; // image, sibling, right sibling int _trackFrameIndex = 0; int _streamedIndex = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp index e50f832c07..db811f91e2 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp @@ -238,6 +238,13 @@ void Pip::RendererGL::deinit(QOpenGLFunctions *f) { _volumeControllerImage.destroy(f); _shadowImage.destroy(f); _textures.destroy(f); + _rgbaSize = QSize(); + _lumaSize = QSize(); + _chromaSize = QSize(); + _chromaSizeV = QSize(); + _chromaNV12 = false; + _trackFrameIndex = 0; + _cacheKey = 0; _imageProgram = std::nullopt; _texturedVertexShader = nullptr; _argb32Program = std::nullopt; @@ -337,8 +344,8 @@ void Pip::RendererGL::paintTransformedVideoFrame( nv12changed ? QSize() : _chromaSize, yuv->u.stride / (nv12 ? 2 : 1), yuv->u.data); + _chromaSize = yuv->chromaSize; if (nv12) { - _chromaSize = yuv->chromaSize; _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4); } _chromaNV12 = nv12; @@ -351,10 +358,10 @@ void Pip::RendererGL::paintTransformedVideoFrame( GL_ALPHA, GL_ALPHA, yuv->chromaSize, - _chromaSize, + _chromaSizeV, yuv->v.stride, yuv->v.data); - _chromaSize = yuv->chromaSize; + _chromaSizeV = yuv->chromaSize; _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4); } } diff --git a/Telegram/SourceFiles/media/view/media_view_pip_opengl.h b/Telegram/SourceFiles/media/view/media_view_pip_opengl.h index 00b296391b..351cfb9eec 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_pip_opengl.h @@ -102,7 +102,8 @@ private: Ui::GL::Textures<4> _textures; QSize _rgbaSize; QSize _lumaSize; - QSize _chromaSize; + QSize _chromaSize; // size of texture 2 (UV for NV12, U for YUV420) + QSize _chromaSizeV; // size of texture 3 (V for YUV420 only) quint64 _cacheKey = 0; int _trackFrameIndex = 0; bool _chromaNV12 = false; From a0c90a3a2e13fbe23aea43735000307be914f71c Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 3 May 2026 10:29:02 +0700 Subject: [PATCH 072/154] Fix window activation on oauth link processing. --- .../SourceFiles/core/local_url_handlers.cpp | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 693b19c6f1..1ae2a02ee3 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -2002,46 +2002,64 @@ QString TryConvertUrlToLocal(QString url) { return url; } -bool InternalPassportOrOAuthLink(const QString &url) { +struct InternalLinkCheckResult { + QString command; + QString username; +}; + +[[nodiscard]] InternalLinkCheckResult InternalLinkCheck(const QString &url) { const auto urlTrimmed = url.trimmed(); if (!urlTrimmed.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { - return false; + return {}; } const auto command = base::StringViewMid(urlTrimmed, u"tg://"_q.size()); using namespace qthelp; const auto matchOptions = RegExOption::CaseInsensitive; - const auto authMatch = regex_match( - u"^passport/?\\?(.+)(#|$)"_q, - command, - matchOptions); - const auto oauthMatch = regex_match( - u"^oauth/?\\?(.+)(#|$)"_q, - command, - matchOptions); const auto usernameMatch = regex_match( u"^resolve/?\\?(.+)(#|$)"_q, command, matchOptions); - auto usernameValue = QString(); + auto username = QString(); if (usernameMatch->hasMatch()) { const auto params = url_parse_params( usernameMatch->captured(1), UrlParamNameTransform::ToLower); - usernameValue = params.value(u"domain"_q); + username = params.value(u"domain"_q); } - const auto authLegacy = (usernameValue == u"telegrampassport"_q); - const auto oauthLegacy = (usernameValue == u"oauth"_q); - return authMatch->hasMatch() + return { .command = command.toString(), .username = username }; +} + +bool InternalPassportLink(const QString &url) { + const auto result = InternalLinkCheck(url); + + using namespace qthelp; + const auto matchOptions = RegExOption::CaseInsensitive; + const auto authMatch = regex_match( + u"^passport/?\\?(.+)(#|$)"_q, + result.command, + matchOptions); + const auto authLegacy = (result.username == u"telegrampassport"_q); + return authMatch->hasMatch() || authLegacy; +} + +bool InternalPassportOrOAuthLink(const QString &url) { + const auto result = InternalLinkCheck(url); + + using namespace qthelp; + const auto matchOptions = RegExOption::CaseInsensitive; + const auto oauthMatch = regex_match( + u"^oauth/?\\?(.+)(#|$)"_q, + result.command, + matchOptions); + const auto oauthLegacy = (result.username == u"oauth"_q); + return InternalPassportLink(url) || oauthMatch->hasMatch() - || authLegacy || oauthLegacy; } bool StartUrlRequiresActivate(const QString &url) { - return Core::App().passcodeLocked() - ? true - : !InternalPassportOrOAuthLink(url); + return Core::App().passcodeLocked() || !InternalPassportLink(url); } void ResolveAndShowUniqueGift( From b40397700d634fd2d506fde61c9d976b1b30c320 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 4 May 2026 11:05:56 +0700 Subject: [PATCH 073/154] Fix tg:// links handling on cold start. --- Telegram/SourceFiles/core/application.cpp | 20 ++++++++- Telegram/SourceFiles/mainwidget.cpp | 49 +++++++++-------------- Telegram/SourceFiles/mainwidget.h | 1 + Telegram/SourceFiles/mainwindow.cpp | 8 +++- Telegram/SourceFiles/mainwindow.h | 4 +- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 6840b9eb35..ca9a3775c3 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -1154,7 +1154,25 @@ void Application::checkStartUrls() { if (!cRefStartUrls().isEmpty() && _lastActivePrimaryWindow && !_lastActivePrimaryWindow->locked()) { - _lastActivePrimaryWindow->widget()->sendPaths(); + auto interprets = QStringList(); + auto paths = QStringList(); + cRefStartUrls() = ranges::views::all( + cRefStartUrls() + ) | ranges::views::filter([&](const QUrl &url) { + if (url.scheme() == u"interpret"_q) { + interprets.append(url.path()); + return false; + } else if (url.isLocalFile()) { + paths.append(url.toLocalFile()); + return false; + } + return true; + }) | ranges::to>; + if (!interprets.isEmpty() || !paths.isEmpty()) { + _lastActivePrimaryWindow->widget()->handleStartFiles( + std::move(interprets), + std::move(paths)); + } } } diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 36135477ff..6e48b3425f 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -2940,35 +2940,7 @@ void MainWidget::activate() { if (_showAnimation) { return; } - const auto urls = base::take(cRefStartUrls()); - const auto interprets = urls | ranges::views::filter([](const QUrl &url) { - return url.scheme() == u"interpret"_q; - }) | ranges::views::transform([](const QUrl &url) { - return url.path(); - }) | ranges::to; - const auto paths = urls | ranges::views::filter( - &QUrl::isLocalFile - ) | ranges::views::transform( - &QUrl::toLocalFile - ) | ranges::to; - if (!interprets.isEmpty() || !paths.isEmpty()) { - if (!interprets.isEmpty()) { - for (const auto &interpret : interprets) { - const auto error = Support::InterpretSendPath( - _controller, - interpret); - if (!error.isEmpty()) { - _controller->show(Ui::MakeInformBox(error)); - } - } - } - if (!paths.isEmpty()) { - const auto chosen = [=](not_null thread) { - return sendPaths(thread, paths); - }; - Window::ShowChooseRecipientBox(_controller, chosen); - } - } else if (_mainSection) { + if (_mainSection) { _mainSection->setInnerFocus(); } else if (_hider) { Assert(_dialogs != nullptr); @@ -2984,6 +2956,25 @@ void MainWidget::activate() { _controller->widget()->fixOrder(); } +void MainWidget::handleStartFiles( + QStringList interprets, + QStringList paths) { + for (const auto &interpret : interprets) { + const auto error = Support::InterpretSendPath( + _controller, + interpret); + if (!error.isEmpty()) { + _controller->show(Ui::MakeInformBox(error)); + } + } + if (!paths.isEmpty()) { + const auto chosen = [=](not_null thread) { + return sendPaths(thread, paths); + }; + Window::ShowChooseRecipientBox(_controller, chosen); + } +} + bool MainWidget::animatingShow() const { return _showAnimation != nullptr; } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 996726c544..d3c03fc669 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -127,6 +127,7 @@ public: void showAnimated(QPixmap oldContentCache, bool back = false); void activate(); + void handleStartFiles(QStringList interprets, QStringList paths); void windowShown(); diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp index 0bfe98a19d..cc30dc043b 100644 --- a/Telegram/SourceFiles/mainwindow.cpp +++ b/Telegram/SourceFiles/mainwindow.cpp @@ -751,14 +751,18 @@ void MainWindow::updateControlsGeometry() { if (_main) _main->checkMainSectionToLayer(); } -void MainWindow::sendPaths() { +void MainWindow::handleStartFiles( + QStringList interprets, + QStringList paths) { if (controller().locked()) { return; } Core::App().hideMediaView(); ui_hideSettingsAndLayer(anim::type::instant); if (_main) { - _main->activate(); + _main->handleStartFiles( + std::move(interprets), + std::move(paths)); } } diff --git a/Telegram/SourceFiles/mainwindow.h b/Telegram/SourceFiles/mainwindow.h index 22eeadbd53..ea36b41e56 100644 --- a/Telegram/SourceFiles/mainwindow.h +++ b/Telegram/SourceFiles/mainwindow.h @@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_main_window.h" #include "ui/layers/layer_widget.h" +#include + class MainWidget; namespace Intro { @@ -66,7 +68,7 @@ public: bool takeThirdSectionFromLayer(); - void sendPaths(); + void handleStartFiles(QStringList interprets, QStringList paths); [[nodiscard]] bool contentOverlapped(const QRect &globalRect); [[nodiscard]] bool contentOverlapped(QWidget *w, QPaintEvent *e) { From 690d9209eebef6b2d4efc1778b2a100b9fa6d55f Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 4 May 2026 11:08:30 +0700 Subject: [PATCH 074/154] Fix tilde in font, remove workaround. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index cac647adb4..2485c73fa0 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit cac647adb486ab980073ab1209821617e590aa3b +Subproject commit 2485c73fa0d5bd387a218461467d048ba7203f6a From a01ddad67b4852c3dc7d0f5965bcd99b7d3984eb Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 8 Apr 2026 08:27:23 +0700 Subject: [PATCH 075/154] Update API scheme to layer 225. --- Telegram/SourceFiles/data/data_poll.cpp | 1 + Telegram/SourceFiles/mtproto/scheme/api.tl | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index e3124bb59a..8ef9149883 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -542,6 +542,7 @@ MTPPoll PollDataToMTP(not_null poll, bool close) { MTP_vector(answers), MTP_int(poll->closePeriod), MTP_int(poll->closeDate), + MTP_vector(), // countries_iso2 MTP_long(0)); } diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index ca55a99a10..0caaab2f5f 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1232,11 +1232,11 @@ help.userInfo#1eb3758 message:string entities:Vector author:strin pollAnswer#4b7d786a flags:# text:TextWithEntities option:bytes media:flags.0?MessageMedia added_by:flags.1?Peer date:flags.1?int = PollAnswer; inputPollAnswer#199fed96 flags:# text:TextWithEntities media:flags.0?InputMedia = PollAnswer; -poll#b8425be9 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true open_answers:flags.6?true revoting_disabled:flags.7?true shuffle_answers:flags.8?true hide_results_until_close:flags.9?true creator:flags.10?true question:TextWithEntities answers:Vector close_period:flags.4?int close_date:flags.5?int hash:long = Poll; +poll#966e2dbf id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true open_answers:flags.6?true revoting_disabled:flags.7?true shuffle_answers:flags.8?true hide_results_until_close:flags.9?true creator:flags.10?true subscribers_only:flags.11?true question:TextWithEntities answers:Vector close_period:flags.4?int close_date:flags.5?int countries_iso2:flags.12?Vector hash:long = Poll; pollAnswerVoters#3645230a flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:flags.2?int recent_voters:flags.2?Vector = PollAnswerVoters; -pollResults#ba7bb15e flags:# min:flags.0?true has_unread_votes:flags.6?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector solution_media:flags.5?MessageMedia = PollResults; +pollResults#ba7bb15e flags:# min:flags.0?true has_unread_votes:flags.6?true can_view_stats:flags.7?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector solution_media:flags.5?MessageMedia = PollResults; chatOnlines#f041e250 onlines:int = ChatOnlines; @@ -2147,6 +2147,16 @@ bots.requestedButton#f13bbcd7 webapp_req_id:string = bots.RequestedButton; messages.composedMessageWithAI#90d7adfa flags:# result_text:TextWithEntities diff_text:flags.0?TextWithEntities = messages.ComposedMessageWithAI; +channels.found#3128c4bc flags:# results:Vector chats:Vector users:Vector next_offset:flags.0?string = channels.Found; + +personalChannel#19bc407d user_id:long channel_id:long = PersonalChannel; + +channels.personalChannels#d69ae84d channels:Vector chats:Vector users:Vector = channels.PersonalChannels; + +channelTopic#93a5df73 id:int title:string = ChannelTopic; + +stats.pollStats#2999beed votes_graph:StatsGraph = stats.PollStats; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2704,6 +2714,9 @@ channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Upda channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool; +channels.search#27d79557 flags:# q:flags.0?string topic_id:flags.1?int offset:string = channels.Found; +channels.getContactPersonalChannels#509b3c66 = channels.PersonalChannels; +channels.getTopics#7ab18dcc lang_code:string = Vector; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2878,6 +2891,7 @@ stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:s stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats; stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards; +stats.getPollStats#c27dfa68 flags:# dark:flags.0?true peer:InputPeer msg_id:int = stats.PollStats; chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector = chatlists.ExportedChatlistInvite; chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool; @@ -2941,4 +2955,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 224 +// LAYER 225 From acd10b1e1dcecc407c0dd06df95c0436c75df4cd Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 10:57:43 +0300 Subject: [PATCH 076/154] Added subscriber-only poll restrictions in creation flow and MTProto flags --- .../poll/filled/filled_poll_subscribers.svg | 7 +++++ Telegram/Resources/langs/lang.strings | 2 ++ .../SourceFiles/boxes/create_poll_box.cpp | 27 ++++++++++++++++--- Telegram/SourceFiles/boxes/polls.style | 1 + Telegram/SourceFiles/data/data_poll.cpp | 11 +++++++- Telegram/SourceFiles/data/data_poll.h | 2 ++ 6 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 Telegram/Resources/icons/poll/filled/filled_poll_subscribers.svg diff --git a/Telegram/Resources/icons/poll/filled/filled_poll_subscribers.svg b/Telegram/Resources/icons/poll/filled/filled_poll_subscribers.svg new file mode 100644 index 0000000000..52b73e4d19 --- /dev/null +++ b/Telegram/Resources/icons/poll/filled/filled_poll_subscribers.svg @@ -0,0 +1,7 @@ + + + Filled / filled_poll_subscribers + + + + diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9363db85fa..4af5e88359 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6995,6 +6995,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_create_poll_ends" = "Poll ends"; "lng_polls_create_hide_results" = "Hide results"; "lng_polls_create_hide_results_about" = "If you switch this on, results will appear only after the poll closes."; +"lng_polls_create_restrict_to_subscribers" = "Restrict to Subscribers"; +"lng_polls_create_restrict_to_subscribers_about" = "Only subscribers who joined 24+ hours ago can vote."; "lng_polls_create_duration_custom" = "Custom"; "lng_polls_create_deadline_title" = "Deadline"; "lng_polls_create_deadline_button" = "Set Deadline"; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index bbd253a4d2..5266267e82 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -2537,6 +2537,7 @@ object_ptr CreatePollBox::setupContent() { Ui::AddSkip(container); Ui::AddSubsectionTitle(container, tr::lng_polls_create_settings()); + const auto isBroadcastChannel = _peer->isChannel(); const auto showWhoVoted = (!(_disabled & PollData::Flag::PublicVotes)) ? AddPollToggleButton( @@ -2732,10 +2733,18 @@ object_ptr CreatePollBox::setupContent() { st::settingsButtonNoIcon) )->toggleOn(rpl::single(false)); - Ui::AddSkip(durationInner); - Ui::AddDividerText( - durationInner, - tr::lng_polls_create_hide_results_about()); + const auto restrictToSubscribers = isBroadcastChannel + ? AddPollToggleButton( + container, + tr::lng_polls_create_restrict_to_subscribers(), + tr::lng_polls_create_restrict_to_subscribers_about(), + { + .icon = &st::pollBoxFilledPollSubscribersIcon, + .background = &st::settingsIconBg4, + }, + rpl::single(!!(_chosen & PollData::Flag::SubscribersOnly)), + st::detailedSettingsButtonStyle).get() + : nullptr; const auto solution = setupSolution( container, @@ -2775,6 +2784,10 @@ object_ptr CreatePollBox::setupContent() { }; quiz->setToggleLocked(_disabled & PollData::Flag::Quiz); shuffle->setToggleLocked(_disabled & PollData::Flag::ShuffleAnswers); + if (restrictToSubscribers) { + restrictToSubscribers->setToggleLocked( + _disabled & PollData::Flag::SubscribersOnly); + } updateQuizDependentLocks(quiz->toggled()); using namespace rpl::mappers; @@ -2862,6 +2875,8 @@ object_ptr CreatePollBox::setupContent() { } const auto publicVotes = (showWhoVoted && showWhoVoted->toggled()); const auto multiChoice = multiple->toggled(); + const auto subscribersOnly = (restrictToSubscribers + && restrictToSubscribers->toggled()); const auto hideResultsEnabled = duration->toggled() && hideResults->toggled(); result.setFlags(Flag(0) @@ -2871,6 +2886,7 @@ object_ptr CreatePollBox::setupContent() { | (!revoting->toggled() ? Flag::RevotingDisabled : Flag(0)) | (shuffle->toggled() ? Flag::ShuffleAnswers : Flag(0)) | (quiz->toggled() ? Flag::Quiz : Flag(0)) + | (subscribersOnly ? Flag::SubscribersOnly : Flag(0)) | (hideResultsEnabled ? Flag::HideResultsUntilClose : Flag(0))); @@ -3049,6 +3065,9 @@ object_ptr CreatePollBox::setupContent() { duration->finishAnimating(); durationWrap->finishAnimating(); hideResults->finishAnimating(); + if (restrictToSubscribers) { + restrictToSubscribers->finishAnimating(); + } return result; } diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index b68834fc16..c1846047b2 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -136,6 +136,7 @@ pollBoxFilledPollCorrectIcon: icon{{ "poll/filled/filled_poll_correct", activeBu pollBoxFilledPollRevoteIcon: icon{{ "poll/filled/filled_poll_revote", activeButtonFg }}; pollBoxFilledPollShuffleIcon: icon{{ "poll/filled/filled_poll_shuffle", activeButtonFg }}; pollBoxFilledPollMultipleIcon: icon{{ "poll/filled/filled_poll_multiple", activeButtonFg }}; +pollBoxFilledPollSubscribersIcon: icon{{ "poll/filled/filled_poll_subscribers-20x20", activeButtonFg }}; pollAttachTextSkip: 28px; pollAttachProgressMargin: 4px; diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 8ef9149883..583377f3bf 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -105,7 +105,8 @@ bool PollData::applyChanges(const MTPDpoll &poll) { | (poll.is_hide_results_until_close() ? Flag::HideResultsUntilClose : Flag(0)) - | (poll.is_creator() ? Flag::Creator : Flag(0)); + | (poll.is_creator() ? Flag::Creator : Flag(0)) + | (poll.is_subscribers_only() ? Flag::SubscribersOnly : Flag(0)); const auto newCloseDate = poll.vclose_date().value_or_empty(); const auto newClosePeriod = poll.vclose_period().value_or_empty(); auto newAnswers = ranges::views::all( @@ -368,6 +369,10 @@ bool PollData::creator() const { return (_flags & Flag::Creator); } +bool PollData::subscribersOnly() const { + return (_flags & Flag::SubscribersOnly); +} + QString PollData::debugString() const { auto result = QString(); result += u"Poll #"_q + QString::number(id) + u'\n'; @@ -384,6 +389,9 @@ QString PollData::debugString() const { if (publicVotes()) { result += u"[PublicVotes]"_q; } + if (subscribersOnly()) { + result += u"[SubscribersOnly]"_q; + } if (!result.endsWith(u'\n')) { result += u'\n'; } @@ -531,6 +539,7 @@ MTPPoll PollDataToMTP(not_null poll, bool close) { | (poll->hideResultsUntilClose() ? Flag::f_hide_results_until_close : Flag(0)) + | (poll->subscribersOnly() ? Flag::f_subscribers_only : Flag(0)) | (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0)) | (poll->closeDate > 0 ? Flag::f_close_date : Flag(0)); return MTP_poll( diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 03760f8771..cbc7d3713b 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -76,6 +76,7 @@ struct PollData { OpenAnswers = 0x040, HideResultsUntilClose = 0x080, Creator = 0x100, + SubscribersOnly = 0x200, }; friend inline constexpr bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -101,6 +102,7 @@ struct PollData { [[nodiscard]] bool openAnswers() const; [[nodiscard]] bool hideResultsUntilClose() const; [[nodiscard]] bool creator() const; + [[nodiscard]] bool subscribersOnly() const; [[nodiscard]] QString debugString() const; From 3ce201593c4e425aaa5d9130d606f69e39409769 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 10:18:29 +0300 Subject: [PATCH 077/154] Added country-based poll restrictions with selectable allowed countries --- .../icons/poll/filled/filled_poll_country.svg | 13 +++ Telegram/Resources/langs/lang.strings | 6 ++ .../SourceFiles/boxes/create_poll_box.cpp | 87 +++++++++++++++++++ Telegram/SourceFiles/boxes/create_poll_box.h | 1 + Telegram/SourceFiles/boxes/polls.style | 1 + Telegram/SourceFiles/data/data_poll.cpp | 26 +++++- Telegram/SourceFiles/data/data_poll.h | 1 + 7 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 Telegram/Resources/icons/poll/filled/filled_poll_country.svg diff --git a/Telegram/Resources/icons/poll/filled/filled_poll_country.svg b/Telegram/Resources/icons/poll/filled/filled_poll_country.svg new file mode 100644 index 0000000000..bf77eb160f --- /dev/null +++ b/Telegram/Resources/icons/poll/filled/filled_poll_country.svg @@ -0,0 +1,13 @@ + + + Filled / filled_poll_country + + + + + + + + + + diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4af5e88359..e9b9151bd1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6997,6 +6997,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_create_hide_results_about" = "If you switch this on, results will appear only after the poll closes."; "lng_polls_create_restrict_to_subscribers" = "Restrict to Subscribers"; "lng_polls_create_restrict_to_subscribers_about" = "Only subscribers who joined 24+ hours ago can vote."; +"lng_polls_create_limit_by_country" = "Limit by Country"; +"lng_polls_create_limit_by_country_about" = "Only users from selected countries can vote."; +"lng_polls_create_allowed_countries" = "Allowed Countries"; +"lng_polls_create_countries_count#one" = "{count} country"; +"lng_polls_create_countries_count#other" = "{count} countries"; +"lng_polls_create_choose_country" = "Please choose at least one country."; "lng_polls_create_duration_custom" = "Custom"; "lng_polls_create_deadline_title" = "Deadline"; "lng_polls_create_deadline_button" = "Set Deadline"; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 5266267e82..5dc27e963e 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "base/random.h" #include "base/unique_qptr.h" +#include "countries/countries_instance.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "chat_helpers/tabbed_panel.h" @@ -38,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "history/view/media/menu/history_view_poll_menu.h" #include "history/view/history_view_schedule_box.h" +#include "info/channel_statistics/boosts/giveaway/select_countries_box.h" #include "lang/lang_keys.h" #include "layout/layout_document_generic_preview.h" #include "main/main_app_config.h" @@ -1458,6 +1460,7 @@ object_ptr CreatePollBox::setupContent() { rpl::event_stream showWhoVotedForceOn; rpl::variable closePeriod = 0; rpl::variable closeDate = TimeId(0); + rpl::variable> countriesValue; std::shared_ptr descriptionMedia = std::make_shared(); std::shared_ptr solutionMedia @@ -2745,6 +2748,68 @@ object_ptr CreatePollBox::setupContent() { rpl::single(!!(_chosen & PollData::Flag::SubscribersOnly)), st::detailedSettingsButtonStyle).get() : nullptr; + const auto limitByCountry = isBroadcastChannel + ? AddPollToggleButton( + container, + tr::lng_polls_create_limit_by_country(), + tr::lng_polls_create_limit_by_country_about(), + { + .icon = &st::pollBoxFilledPollCountryIcon, + .background = &st::settingsIconBg6, + }, + rpl::single(false), + st::detailedSettingsButtonStyle).get() + : nullptr; + const auto countriesWrap = limitByCountry + ? container->add( + object_ptr>( + container, + object_ptr(container))) + : nullptr; + const auto countriesButton = [=] { + if (!countriesWrap) { + return (Ui::SettingsButton*)(nullptr); + } + const auto inner = countriesWrap->entity(); + return AddButtonWithLabel( + inner, + tr::lng_polls_create_allowed_countries(), + state->countriesValue.value( + ) | rpl::map([=](const std::vector &countries) { + if (countries.empty()) { + return QString(); + } + if (countries.size() == 1) { + return Countries::Instance().countryNameByISO2( + countries.front()); + } + return tr::lng_polls_create_countries_count( + tr::now, + lt_count, + countries.size()); + }), + st::settingsButtonNoIcon).get(); + }(); + if (countriesWrap) { + countriesWrap->toggleOn( + rpl::single(limitByCountry->toggled()) + | rpl::then(limitByCountry->toggledChanges())); + } + if (countriesButton) { + countriesButton->setClickedCallback([=] { + const auto done = [=](std::vector countries) { + state->countriesValue = std::move(countries); + }; + const auto checkError = [](int) { + return false; + }; + show->show(Box( + Ui::SelectCountriesBox, + state->countriesValue.current(), + done, + checkError)); + }); + } const auto solution = setupSolution( container, @@ -2879,6 +2944,10 @@ object_ptr CreatePollBox::setupContent() { && restrictToSubscribers->toggled()); const auto hideResultsEnabled = duration->toggled() && hideResults->toggled(); + result.countries = (limitByCountry + && limitByCountry->toggled()) + ? state->countriesValue.current() + : std::vector(); result.setFlags(Flag(0) | (publicVotes ? Flag::PublicVotes : Flag(0)) | (multiChoice ? Flag::MultiChoice : Flag(0)) @@ -2950,6 +3019,13 @@ object_ptr CreatePollBox::setupContent() { } else { state->error &= ~Error::Deadline; } + if (limitByCountry + && limitByCountry->toggled() + && state->countriesValue.current().empty()) { + state->error |= Error::Country; + } else { + state->error &= ~Error::Country; + } }; const auto showError = [show = uiShow()]( tr::phrase<> text) { @@ -3009,6 +3085,11 @@ object_ptr CreatePollBox::setupContent() { ShowMediaUploadingToast(); } else if (state->error & Error::Deadline) { showError(tr::lng_polls_create_deadline_expired); + } else if (state->error & Error::Country) { + showError(tr::lng_polls_create_choose_country); + if (countriesButton) { + scrollToWidget(countriesButton); + } } else if (!state->error) { auto result = collectResult(); result.options = sendOptions; @@ -3068,6 +3149,12 @@ object_ptr CreatePollBox::setupContent() { if (restrictToSubscribers) { restrictToSubscribers->finishAnimating(); } + if (limitByCountry) { + limitByCountry->finishAnimating(); + } + if (countriesWrap) { + countriesWrap->finishAnimating(); + } return result; } diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h index 6940101697..1bd2518200 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.h +++ b/Telegram/SourceFiles/boxes/create_poll_box.h @@ -68,6 +68,7 @@ private: Solution = 0x10, Media = 0x20, Deadline = 0x40, + Country = 0x80, }; friend constexpr inline bool is_flag_type(Error) { return true; } using Errors = base::flags; diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index c1846047b2..f5f07922fa 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -137,6 +137,7 @@ pollBoxFilledPollRevoteIcon: icon{{ "poll/filled/filled_poll_revote", activeButt pollBoxFilledPollShuffleIcon: icon{{ "poll/filled/filled_poll_shuffle", activeButtonFg }}; pollBoxFilledPollMultipleIcon: icon{{ "poll/filled/filled_poll_multiple", activeButtonFg }}; pollBoxFilledPollSubscribersIcon: icon{{ "poll/filled/filled_poll_subscribers-20x20", activeButtonFg }}; +pollBoxFilledPollCountryIcon: icon{{ "poll/filled/filled_poll_country-20x20", activeButtonFg }}; pollAttachTextSkip: 28px; pollAttachProgressMargin: 4px; diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 583377f3bf..23e046aa54 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -109,6 +109,13 @@ bool PollData::applyChanges(const MTPDpoll &poll) { | (poll.is_subscribers_only() ? Flag::SubscribersOnly : Flag(0)); const auto newCloseDate = poll.vclose_date().value_or_empty(); const auto newClosePeriod = poll.vclose_period().value_or_empty(); + auto newCountries = std::vector(); + if (const auto countries = poll.vcountries_iso2()) { + newCountries.reserve(countries->v.size()); + for (const auto &country : countries->v) { + newCountries.push_back(qs(country)); + } + } auto newAnswers = ranges::views::all( poll.vanswers().v ) | ranges::views::transform([&](const MTPPollAnswer &data) { @@ -146,6 +153,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) { const auto changed1 = (question != newQuestion) || (closeDate != newCloseDate) || (closePeriod != newClosePeriod) + || (countries != newCountries) || (_flags != newFlags); const auto changed2 = (answers != newAnswers); if (!changed1 && !changed2) { @@ -155,6 +163,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) { question = newQuestion; closeDate = newCloseDate; closePeriod = newClosePeriod; + countries = std::move(newCountries); _flags = newFlags; } if (changed2) { @@ -410,6 +419,13 @@ QString PollData::debugString() const { if (!solution.text.isEmpty()) { result += u"Solution: "_q + solution.text + u'\n'; } + if (!countries.empty()) { + result += u"Countries: "_q + countries.front(); + for (auto i = 1, count = int(countries.size()); i != count; ++i) { + result += u", "_q + countries[i]; + } + result += u'\n'; + } return result; } @@ -528,6 +544,11 @@ MTPPoll PollDataToMTP(not_null poll, bool close) { poll->answers, ranges::back_inserter(answers), convert); + auto countries = QVector(); + countries.reserve(poll->countries.size()); + for (const auto &country : poll->countries) { + countries.push_back(MTP_string(country)); + } using Flag = MTPDpoll::Flag; const auto flags = ((poll->closed() || close) ? Flag::f_closed : Flag(0)) | (poll->multiChoice() ? Flag::f_multiple_choice : Flag(0)) @@ -541,7 +562,8 @@ MTPPoll PollDataToMTP(not_null poll, bool close) { : Flag(0)) | (poll->subscribersOnly() ? Flag::f_subscribers_only : Flag(0)) | (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0)) - | (poll->closeDate > 0 ? Flag::f_close_date : Flag(0)); + | (poll->closeDate > 0 ? Flag::f_close_date : Flag(0)) + | (countries.isEmpty() ? Flag(0) : Flag::f_countries_iso2); return MTP_poll( MTP_long(poll->id), MTP_flags(flags), @@ -551,7 +573,7 @@ MTPPoll PollDataToMTP(not_null poll, bool close) { MTP_vector(answers), MTP_int(poll->closePeriod), MTP_int(poll->closeDate), - MTP_vector(), // countries_iso2 + MTP_vector(std::move(countries)), // countries_iso2 MTP_long(0)); } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index cbc7d3713b..d3059639ce 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -114,6 +114,7 @@ struct PollData { TextWithEntities solution; PollMedia attachedMedia; PollMedia solutionMedia; + std::vector countries; TimeId closePeriod = 0; TimeId closeDate = 0; int totalVoters = 0; From 390fec44b601902b8848e1c22580ac8954cd3657 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 11:28:53 +0300 Subject: [PATCH 078/154] Added poll vote restriction toasts for subscriber and country errors. --- Telegram/Resources/langs/lang.strings | 5 + Telegram/SourceFiles/api/api_polls.cpp | 166 ++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 2 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e9b9151bd1..1cc4c04b89 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7020,6 +7020,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_solution_about" = "Users will see this comment after choosing a wrong answer, good for educational purposes."; "lng_polls_media_uploading_toast_title" = "Please wait"; "lng_polls_media_uploading_toast" = "Poll media is still uploading..."; +"lng_polls_vote_restricted_subscribers_channel" = "Only subscribers of {channel} can vote."; +"lng_polls_vote_restricted_subscribers" = "Only subscribers can vote."; +"lng_polls_vote_restricted_subscribers_recent" = "Only subscribers who joined more than 24 hours ago can vote."; +"lng_polls_vote_restricted_countries_list" = "Only users from {countries} can vote."; +"lng_polls_vote_restricted_countries" = "Only users from selected countries can vote."; "lng_polls_ends_toast" = "Results will appear after the poll ends."; "lng_polls_poll_results_title" = "Poll results"; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 09dcd0a064..733127a910 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "apiwrap.h" #include "base/random.h" +#include "countries/countries_instance.h" #include "data/business/data_shortcut_messages.h" #include "data/data_changes.h" #include "data/data_histories.h" @@ -20,7 +21,164 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" // ShouldSendSilent +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "styles/style_polls.h" +#include "ui/toast/toast.h" +#include "window/window_session_controller.h" + +namespace { + +constexpr auto kVoteRestrictionToastDuration = 5 * crl::time(1000); + +enum class VoteRestrictionError { + None, + SubscribersOnly, + SubscribersJoinedTooRecently, + Countries, +}; + +const auto kSubscribersOnlyVoteErrorPatterns = std::array{ + u"POLL_SUBSCRIBERS_ONLY"_q, + u"POLL_MEMBER_RESTRICTED"_q, + u"VOTE_SUBSCRIBERS_ONLY"_q, + u"SUBSCRIBERS_ONLY"_q, + u"SUBSCRIBER_REQUIRED"_q, + u"SUBSCRIBER_ONLY"_q, +}; + +const auto kSubscribersJoinedTooRecentlyVoteErrorPatterns = std::array{ + u"POLL_SUBSCRIBERS_TOO_RECENT"_q, + u"VOTE_SUBSCRIBERS_TOO_RECENT"_q, + u"SUBSCRIBERS_TOO_RECENT"_q, + u"SUBSCRIBER_TOO_RECENT"_q, + u"JOINED_TOO_RECENTLY"_q, + u"24_HOURS"_q, +}; + +const auto kCountriesVoteErrorPatterns = std::array{ + u"POLL_COUNTRIES_ISO2"_q, + u"VOTE_COUNTRIES_ISO2"_q, + u"COUNTRIES_ISO2"_q, + u"COUNTRY_RESTRICTED"_q, + u"COUNTRY_ISO2"_q, +}; + +template +[[nodiscard]] bool MatchesErrorPattern( + const QString &type, + const std::array &patterns) { + for (const auto &pattern : patterns) { + if (!pattern.isEmpty() + && type.contains(pattern, Qt::CaseInsensitive)) { + return true; + } + } + return false; +} + +[[nodiscard]] VoteRestrictionError ParseVoteRestrictionError( + const QString &type) { + if (MatchesErrorPattern( + type, + kSubscribersJoinedTooRecentlyVoteErrorPatterns)) { + return VoteRestrictionError::SubscribersJoinedTooRecently; + } else if (MatchesErrorPattern( + type, + kSubscribersOnlyVoteErrorPatterns)) { + return VoteRestrictionError::SubscribersOnly; + } else if (MatchesErrorPattern( + type, + kCountriesVoteErrorPatterns)) { + return VoteRestrictionError::Countries; + } + return VoteRestrictionError::None; +} + +[[nodiscard]] QString JoinCountryNames( + const std::vector &countriesIso2) { + auto countries = QStringList(); + countries.reserve(int(countriesIso2.size())); + const auto &instance = Countries::Instance(); + for (const auto &iso2 : countriesIso2) { + const auto name = instance.countryNameByISO2(iso2); + countries.push_back(name.isEmpty() ? iso2 : name); + } + if (countries.empty()) { + return QString(); + } + auto result = countries.front(); + for (auto i = 1, count = int(countries.size()); i != count; ++i) { + result = ((i + 1 == count) + ? tr::lng_prizes_countries_and_last + : tr::lng_prizes_countries_and_one)( + tr::now, + lt_countries, + result, + lt_country, + countries[i]); + } + return result; +} + +[[nodiscard]] TextWithEntities VoteRestrictionToastText( + VoteRestrictionError error, + not_null peer, + not_null poll) { + switch (error) { + case VoteRestrictionError::SubscribersOnly: { + const auto channel = peer->name(); + return channel.isEmpty() + ? tr::lng_polls_vote_restricted_subscribers(tr::now, tr::rich) + : tr::lng_polls_vote_restricted_subscribers_channel( + tr::now, + lt_channel, + tr::bold(channel), + tr::rich); + } + case VoteRestrictionError::SubscribersJoinedTooRecently: + return tr::lng_polls_vote_restricted_subscribers_recent( + tr::now, + tr::rich); + case VoteRestrictionError::Countries: { + const auto countries = JoinCountryNames(poll->countries); + return countries.isEmpty() + ? tr::lng_polls_vote_restricted_countries(tr::now, tr::rich) + : tr::lng_polls_vote_restricted_countries_list( + tr::now, + lt_countries, + tr::bold(countries), + tr::rich); + } + case VoteRestrictionError::None: + break; + } + return {}; +} + +void ShowVoteRestrictionToast( + not_null peer, + not_null poll, + const MTP::Error &error) { + const auto parsed = ParseVoteRestrictionError(error.type()); + if (parsed == VoteRestrictionError::None) { + return; + } + auto text = VoteRestrictionToastText(parsed, peer, poll); + if (text.text.isEmpty()) { + return; + } + if (const auto window = peer->session().tryResolveWindow(peer)) { + window->showToast({ + .text = std::move(text), + .iconLottie = u"ban"_q, + .iconLottieSize = st::pollToastIconSize, + .duration = kVoteRestrictionToastDuration, + }); + } +} + +} // namespace namespace Api { @@ -151,6 +309,7 @@ void Polls::sendVotes( if (!item) { return; } + const auto peer = item->history()->peer; const auto showSending = poll && !options.empty(); const auto hideSending = [=] { @@ -179,16 +338,19 @@ void Polls::sendVotes( ranges::back_inserter(prepared), [](const QByteArray &option) { return MTP_bytes(option); }); const auto requestId = _api.request(MTPmessages_SendVote( - item->history()->peer->input(), + peer->input(), MTP_int(item->id), MTP_vector(prepared) )).done([=](const MTPUpdates &result) { _pollVotesRequestIds.erase(itemId); hideSending(); _session->updates().applyUpdates(result); - }).fail([=] { + }).fail([=](const MTP::Error &error) { _pollVotesRequestIds.erase(itemId); hideSending(); + if (poll) { + ShowVoteRestrictionToast(peer, poll, error); + } }).send(); _pollVotesRequestIds.emplace(itemId, requestId); } From 1773f264ebb4e78eb6d2bb07111aec321c29c8a2 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 11:44:37 +0300 Subject: [PATCH 079/154] Added poll stats action and dialog with API-backed chart loading. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/api/api_polls.cpp | 48 ++++++++ Telegram/SourceFiles/api/api_polls.h | 7 ++ Telegram/SourceFiles/data/data_poll.cpp | 22 ++++ Telegram/SourceFiles/data/data_poll.h | 2 + .../view/history_view_context_menu.cpp | 8 +- .../media/menu/history_view_poll_menu.cpp | 108 ++++++++++++++++++ .../view/media/menu/history_view_poll_menu.h | 4 + 8 files changed, 200 insertions(+), 1 deletion(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1cc4c04b89..55555727c7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6947,7 +6947,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_answers_count#other" = "{count} answers"; "lng_polls_answers_none" = "No answers"; "lng_polls_submit_votes" = "Vote"; +"lng_polls_view_stats" = "View Stats"; "lng_polls_view_results" = "View results"; +"lng_polls_stats_title" = "Poll Stats"; "lng_polls_view_votes#one" = "View Votes ({count})"; "lng_polls_view_votes#other" = "View Votes ({count})"; "lng_polls_admin_votes#one" = "{count} vote {arrow}"; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 733127a910..363b482f50 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_polls.h" #include "api/api_common.h" +#include "api/api_statistics_data_deserialize.h" #include "api/api_text_entities.h" #include "api/api_updates.h" #include "apiwrap.h" @@ -18,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_poll.h" #include "data/data_session.h" +#include "data/data_statistics_chart.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" // ShouldSendSilent @@ -473,4 +475,50 @@ void Polls::reloadResults(not_null item) { _pollReloadRequestIds.emplace(itemId, requestId); } +void Polls::requestStats( + FullMsgId itemId, + Fn done, + Fn fail) { + const auto item = _session->data().message(itemId); + if (!item || !item->isRegular()) { + if (fail) { + fail(QString()); + } + return; + } + const auto requestGraph = [=](const QString &token) { + _api.request(MTPstats_LoadAsyncGraph( + MTP_flags(MTPstats_LoadAsyncGraph::Flag(0)), + MTP_string(token), + MTP_long(0) + )).done([=](const MTPStatsGraph &result) { + if (done) { + done(Api::StatisticalGraphFromTL(result)); + } + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error.type()); + } + }).send(); + }; + _api.request(MTPstats_GetPollStats( + MTP_flags(MTPstats_GetPollStats::Flags(0)), + item->history()->peer->input(), + MTP_int(item->id) + )).done([=](const MTPstats_PollStats &result) { + auto graph = Api::StatisticalGraphFromTL(result.data().vvotes_graph()); + if (graph.chart || !graph.error.isEmpty() || graph.zoomToken.isEmpty()) { + if (done) { + done(std::move(graph)); + } + } else { + requestGraph(graph.zoomToken); + } + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error.type()); + } + }).send(); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_polls.h b/Telegram/SourceFiles/api/api_polls.h index 26d46e09af..a7b1491d5f 100644 --- a/Telegram/SourceFiles/api/api_polls.h +++ b/Telegram/SourceFiles/api/api_polls.h @@ -14,6 +14,9 @@ class ApiWrap; class HistoryItem; struct PollData; struct PollMedia; +namespace Data { +struct StatisticalGraph; +} // namespace Data namespace Main { class Session; @@ -45,6 +48,10 @@ public: void deleteAnswer(FullMsgId itemId, const QByteArray &option); void close(not_null item); void reloadResults(not_null item); + void requestStats( + FullMsgId itemId, + Fn done, + Fn fail); private: const not_null _session; diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 23e046aa54..11b167240a 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -188,6 +188,21 @@ bool PollData::applyResults(const MTPPollResults &results) { const auto newTotalVoters = results.vtotal_voters().value_or(totalVoters); auto changed = (newTotalVoters != totalVoters); + const auto setCanViewStats = [&](bool value) { + const auto previous = (_flags & Flag::CanViewStats); + if (previous == value) { + return; + } + if (value) { + _flags |= Flag::CanViewStats; + } else { + _flags &= ~Flag::CanViewStats; + } + changed = true; + }; + if (!results.is_min() || results.is_can_view_stats()) { + setCanViewStats(results.is_can_view_stats()); + } if (const auto list = results.vresults()) { for (const auto &result : list->v) { if (applyResultToAnswers(result, results.is_min())) { @@ -382,6 +397,10 @@ bool PollData::subscribersOnly() const { return (_flags & Flag::SubscribersOnly); } +bool PollData::canViewStats() const { + return (_flags & Flag::CanViewStats); +} + QString PollData::debugString() const { auto result = QString(); result += u"Poll #"_q + QString::number(id) + u'\n'; @@ -401,6 +420,9 @@ QString PollData::debugString() const { if (subscribersOnly()) { result += u"[SubscribersOnly]"_q; } + if (canViewStats()) { + result += u"[CanViewStats]"_q; + } if (!result.endsWith(u'\n')) { result += u'\n'; } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index d3059639ce..be839ca0e1 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -77,6 +77,7 @@ struct PollData { HideResultsUntilClose = 0x080, Creator = 0x100, SubscribersOnly = 0x200, + CanViewStats = 0x400, }; friend inline constexpr bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -103,6 +104,7 @@ struct PollData { [[nodiscard]] bool hideResultsUntilClose() const; [[nodiscard]] bool creator() const; [[nodiscard]] bool subscribersOnly() const; + [[nodiscard]] bool canViewStats() const; [[nodiscard]] QString debugString() const; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 3bc816d385..234e7e7bf8 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_text.h" #include "history/view/history_view_schedule_box.h" #include "history/view/media/history_view_media.h" +#include "history/view/media/menu/history_view_poll_menu.h" #include "history/view/media/history_view_save_document_action.h" #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions_list.h" @@ -1891,10 +1892,15 @@ void AddPollActions( && (context != Context::ChatPreview)) { return; } + const auto itemId = item->fullId(); + if (poll->canViewStats() && item->isRegular()) { + menu->addAction(tr::lng_polls_view_stats(tr::now), [=] { + ShowPollStatsBox(controller, itemId); + }, &st::menuIconStats); + } if (poll->closed()) { return; } - const auto itemId = item->fullId(); if (!skipRetractVote && poll->voted() && !poll->quiz() diff --git a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp index 2a3774d75c..111a67d3be 100644 --- a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp +++ b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_poll.h" #include "data/data_session.h" +#include "data/data_statistics_chart.h" #include "data/stickers/data_stickers.h" #include "editor/editor_layer_widget.h" #include "editor/photo_editor.h" @@ -29,21 +30,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_document.h" #include "lang/lang_keys.h" #include "layout/layout_document_generic_preview.h" +#include "lottie/lottie_icon.h" #include "main/main_session.h" #include "poll/poll_media_upload.h" +#include "statistics/chart_widget.h" #include "mainwidget.h" +#include "settings/settings_common.h" #include "storage/localimageloader.h" #include "storage/storage_media_prepare.h" #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" #include "ui/chat/attach/attach_prepare.h" +#include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/vertical_list.h" #include "ui/text/format_song_name.h" #include "ui/text/format_values.h" #include "ui/widgets/dropdown_menu.h" +#include "ui/widgets/labels.h" #include "ui/widgets/menu/menu_action.h" #include "ui/widgets/shadow.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" @@ -51,6 +59,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_media_view.h" #include "styles/style_menu_icons.h" +#include "styles/style_settings.h" +#include "styles/style_statistics.h" #include "styles/style_widgets.h" namespace HistoryView { @@ -249,6 +259,104 @@ void FillPollAnswerMenu( } } +void ShowPollStatsBox( + not_null controller, + FullMsgId itemId) { + controller->show(Box([=](not_null box) { + box->setTitle(tr::lng_polls_stats_title()); + box->setWidth(st::boxWideWidth); + + const auto content = box->addRow( + object_ptr(box), + Margins(0)); + const auto loadingWrap = content->add( + object_ptr>( + content, + object_ptr(content))); + loadingWrap->toggle(true, anim::type::instant); + const auto loading = loadingWrap->entity(); + const auto resultWrap = content->add( + object_ptr>( + content, + object_ptr(content))); + resultWrap->toggle(false, anim::type::instant); + const auto result = resultWrap->entity(); + + auto icon = ::Settings::CreateLottieIcon( + loading, + { .name = u"stats"_q, .sizeOverride = st::normalBoxLottieSize }, + st::settingsBlockedListIconPadding); + loading->add(std::move(icon.widget)); + auto startAnimation = std::move(icon.animate); + box->showFinishes( + ) | rpl::take(1) | rpl::on_next([=]() mutable { + startAnimation(anim::repeat::loop); + }, loading->lifetime()); + loading->add( + object_ptr( + loading, + tr::lng_stats_loading(), + st::changePhoneTitle), + st::changePhoneTitlePadding + st::boxRowPadding, + style::al_top); + loading->add( + object_ptr( + loading, + tr::lng_stats_loading_subtext(), + st::statisticsLoadingSubtext), + st::changePhoneDescriptionPadding + st::boxRowPadding, + style::al_top + )->setTryMakeSimilarLines(true); + Ui::AddSkip(loading, st::settingsBlockedListIconPadding.top()); + const auto finishLoading = [=] { + loading->clear(); + loadingWrap->toggle(false, anim::type::instant); + resultWrap->toggle(true, anim::type::instant); + }; + const auto showError = [=](QString error) { + finishLoading(); + result->clear(); + result->add( + object_ptr( + result, + error.isEmpty() + ? tr::lng_polls_votes_none(tr::now) + : std::move(error), + st::defaultFlatLabel), + st::boxRowPadding + st::statisticsLayerMargins, + style::al_center); + }; + + controller->session().api().polls().requestStats( + itemId, + crl::guard(box, [=](Data::StatisticalGraph graph) { + if (graph.chart) { + finishLoading(); + result->clear(); + const auto chart = result->add( + object_ptr(result), + st::statisticsLayerMargins); + chart->setChartData( + std::move(graph.chart), + Statistic::ChartViewType::Linear); + chart->setTitle(tr::lng_notification_reactions_poll_votes()); + Statistic::FixCacheForHighDPIChartWidget(result); + } else { + showError(!graph.error.isEmpty() + ? graph.error + : tr::lng_polls_votes_none(tr::now)); + } + }), + crl::guard(box, [=](QString error) { + showError(std::move(error)); + })); + + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + })); +} + namespace { void AddRemoveAction( diff --git a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h index 8b5765c789..32709bb61f 100644 --- a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h +++ b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h @@ -52,6 +52,10 @@ void FillPollAnswerMenu( FullMsgId itemId, not_null controller); +void ShowPollStatsBox( + not_null controller, + FullMsgId itemId); + void ShowPollStickerPreview( not_null controller, not_null document, From 1865b6c84b9311d02c26b70fdc960397cf63a011 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 11:57:53 +0300 Subject: [PATCH 080/154] Enabled Ctrl for debug config to show mock poll stats. --- Telegram/SourceFiles/api/api_polls.cpp | 115 ++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 363b482f50..6c49dccfbe 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "api/api_updates.h" #include "apiwrap.h" +#include "base/call_delayed.h" +#include "base/qt/qt_key_modifiers.h" #include "base/random.h" #include "countries/countries_instance.h" #include "data/business/data_shortcut_messages.h" @@ -180,6 +182,102 @@ void ShowVoteRestrictionToast( } } +#ifdef _DEBUG +[[nodiscard]] Data::StatisticalGraph GenerateMockupPollStats( + const PollData &poll) { + auto chart = Data::StatisticalChart(); + const auto colorKeys = std::array{ + u"BLUE"_q, + u"GREEN"_q, + u"RED"_q, + u"GOLDEN"_q, + u"LIGHTBLUE"_q, + u"LIGHTGREEN"_q, + u"ORANGE"_q, + u"INDIGO"_q, + u"PURPLE"_q, + u"CYAN"_q, + }; + + constexpr auto kPoints = 14; + constexpr auto kOneDay = float64(24 * 60 * 60 * 1000); + constexpr auto kStart = float64(1704067200000); + chart.x.reserve(kPoints); + for (auto i = 0; i != kPoints; ++i) { + chart.x.push_back(kStart + i * kOneDay); + } + chart.timeStep = kOneDay; + + auto lineId = 0; + chart.lines.reserve(poll.answers.size()); + for (const auto &answer : poll.answers) { + auto line = Data::StatisticalChart::Line(); + line.id = ++lineId; + line.idString = u"answer_%1"_q.arg(line.id); + line.name = answer.text.text.trimmed(); + if (line.name.isEmpty()) { + line.name = QString("#%1").arg(line.id); + } + line.colorKey = colorKeys[(line.id - 1) % int(colorKeys.size())]; + line.y.reserve(kPoints); + + auto seed = int64(13 * line.id + 17); + for (const auto byte : answer.option) { + seed += uchar(byte); + } + const auto base = std::max(int64(answer.votes), int64(1)); + for (auto i = 0; i != kPoints; ++i) { + const auto wave = int64( + ((i + line.id) % 5) * ((i + 2 * line.id) % 4)); + const auto trend = int64((i * (line.id + 1)) / 3); + const auto noise = int64((seed + i * 7 + line.id * 11) % 6); + const auto value = std::max( + base + wave + trend + noise - 2, + int64(1)); + line.y.push_back(value); + line.maxValue = std::max(line.maxValue, value); + line.minValue = std::min(line.minValue, value); + } + chart.lines.push_back(std::move(line)); + } + if (chart.lines.empty()) { + auto line = Data::StatisticalChart::Line(); + line.id = 1; + line.idString = u"votes"_q; + line.name = tr::lng_notification_reactions_poll_votes(tr::now); + line.colorKey = u"BLUE"_q; + line.y.reserve(kPoints); + + const auto base = std::max(int64(poll.totalVoters), int64(1)); + for (auto i = 0; i != kPoints; ++i) { + const auto value = std::max( + base + i * 2 + ((i * 5) % 7), + int64(1)); + line.y.push_back(value); + line.maxValue = std::max(line.maxValue, value); + line.minValue = std::min(line.minValue, value); + } + chart.lines.push_back(std::move(line)); + } + + chart.defaultZoomXIndex = { + .min = std::max(0, kPoints - 8), + .max = kPoints - 1, + }; + chart.measure(); + if (chart.maxValue == chart.minValue) { + if (chart.minValue) { + chart.minValue = 0; + } else { + chart.maxValue = 1; + } + } + return { + .chart = std::move(chart), + }; +} +#endif + } // namespace namespace Api { @@ -480,12 +578,27 @@ void Polls::requestStats( Fn done, Fn fail) { const auto item = _session->data().message(itemId); - if (!item || !item->isRegular()) { + const auto media = item ? item->media() : nullptr; + const auto poll = media ? media->poll() : nullptr; + if (!item || !item->isRegular() || !poll) { if (fail) { fail(QString()); } return; } +#ifdef _DEBUG + if (base::IsCtrlPressed()) { + auto callback = std::move(done); + if (callback) { + constexpr auto kMockupStatsDelay = 2 * crl::time(1000); + auto graph = GenerateMockupPollStats(*poll); + base::call_delayed(kMockupStatsDelay, _session, [=]() mutable { + callback(std::move(graph)); + }); + } + return; + } +#endif const auto requestGraph = [=](const QString &token) { _api.request(MTPstats_LoadAsyncGraph( MTP_flags(MTPstats_LoadAsyncGraph::Flag(0)), From bb972b2755cfd2c9261a74fbea6dcb3671295a5a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 12:28:09 +0300 Subject: [PATCH 081/154] Fixed long chart filter labels in statistics. --- .../widgets/chart_lines_filter_widget.cpp | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/Telegram/SourceFiles/statistics/widgets/chart_lines_filter_widget.cpp b/Telegram/SourceFiles/statistics/widgets/chart_lines_filter_widget.cpp index a6dfa45c23..4b5c0a7208 100644 --- a/Telegram/SourceFiles/statistics/widgets/chart_lines_filter_widget.cpp +++ b/Telegram/SourceFiles/statistics/widgets/chart_lines_filter_widget.cpp @@ -25,6 +25,7 @@ public: const QString &text, QColor activeColor); + void setOuterWidth(int outerWidth); void shake(); void setChecked(bool value, bool animated); [[nodiscard]] bool checked() const; @@ -45,6 +46,7 @@ private: int shift = 0; } _shake; + int _naturalWidth = 0; bool _checked = true; }; @@ -60,13 +62,19 @@ ChartLinesFilterWidget::FlatCheckbox::FlatCheckbox( , _text(st::statisticsDetailsPopupStyle, text) { const auto &margins = st::statisticsChartFlatCheckboxMargins; const auto h = _text.minHeight() + rect::m::sum::v(margins) * 2; - resize( - _text.maxWidth() - + rect::m::sum::h(margins) - + h - + st::statisticsChartFlatCheckboxCheckWidth * 3 - - st::statisticsChartFlatCheckboxShrinkkWidth, - h); + _naturalWidth = _text.maxWidth() + + rect::m::sum::h(margins) + + h + + st::statisticsChartFlatCheckboxCheckWidth * 3 + - st::statisticsChartFlatCheckboxShrinkkWidth; + resize(_naturalWidth, h); +} + +void ChartLinesFilterWidget::FlatCheckbox::setOuterWidth(int outerWidth) { + const auto newWidth = std::max(std::min(_naturalWidth, outerWidth), 1); + if (width() != newWidth) { + resize(newWidth, height()); + } } void ChartLinesFilterWidget::FlatCheckbox::setChecked( @@ -108,12 +116,19 @@ void ChartLinesFilterWidget::FlatCheckbox::paintEvent(QPaintEvent *e) { const auto checkWidth = st::statisticsChartFlatCheckboxCheckWidth; const auto r = rect() - st::statisticsChartFlatCheckboxMargins; - const auto heightHalf = r.height() / 2.; - const auto textX = anim::interpolate( - r.center().x() - _text.maxWidth() / 2., - r.x() + heightHalf + checkWidth * 5, - progress); + const auto heightHalf = r.height() / 2; + const auto constrained = (width() < _naturalWidth); + const auto checkedTextX = r.x() + heightHalf + checkWidth * 5; + const auto textX = constrained + ? checkedTextX + : anim::interpolate( + r.center().x() - _text.maxWidth() / 2., + checkedTextX, + progress); const auto textY = (r - st::statisticsChartFlatCheckboxMargins).y(); + const auto textWidth = constrained + ? std::max(r.width() - (textX - r.x()) - heightHalf, 0) + : width(); p.fillRect(r, Qt::transparent); constexpr auto kCheckPartProgress = 0.5; @@ -136,7 +151,8 @@ void ChartLinesFilterWidget::FlatCheckbox::paintEvent(QPaintEvent *e) { p.setPen(textColor); const auto textContext = Ui::Text::PaintContext{ .position = QPoint(textX, textY), - .availableWidth = width(), + .availableWidth = textWidth, + .elisionLines = constrained ? 1 : 0, }; _text.draw(p, textContext); @@ -162,6 +178,7 @@ void ChartLinesFilterWidget::resizeToWidth(int outerWidth) { auto maxRight = 0; for (auto i = 0; i < _buttons.size(); i++) { const auto raw = _buttons[i].get(); + raw->setOuterWidth(outerWidth); if (!i) { raw->move(0, 0); } else { From 9e2f782df151934258870cc634e9a21f0e2a9a77 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 12:34:44 +0300 Subject: [PATCH 082/154] Added poll vote restriction info to context menu. --- Telegram/SourceFiles/api/api_polls.cpp | 39 +---------- Telegram/SourceFiles/data/data_poll.cpp | 39 +++++++++++ Telegram/SourceFiles/data/data_poll.h | 5 ++ .../history/history_inner_widget.cpp | 10 ++- .../view/history_view_context_menu.cpp | 65 +++++++++++++++++-- .../history/view/history_view_context_menu.h | 4 ++ 6 files changed, 118 insertions(+), 44 deletions(-) diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 6c49dccfbe..a713643893 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -15,7 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/call_delayed.h" #include "base/qt/qt_key_modifiers.h" #include "base/random.h" -#include "countries/countries_instance.h" #include "data/business/data_shortcut_messages.h" #include "data/data_changes.h" #include "data/data_histories.h" @@ -99,32 +98,6 @@ template return VoteRestrictionError::None; } -[[nodiscard]] QString JoinCountryNames( - const std::vector &countriesIso2) { - auto countries = QStringList(); - countries.reserve(int(countriesIso2.size())); - const auto &instance = Countries::Instance(); - for (const auto &iso2 : countriesIso2) { - const auto name = instance.countryNameByISO2(iso2); - countries.push_back(name.isEmpty() ? iso2 : name); - } - if (countries.empty()) { - return QString(); - } - auto result = countries.front(); - for (auto i = 1, count = int(countries.size()); i != count; ++i) { - result = ((i + 1 == count) - ? tr::lng_prizes_countries_and_last - : tr::lng_prizes_countries_and_one)( - tr::now, - lt_countries, - result, - lt_country, - countries[i]); - } - return result; -} - [[nodiscard]] TextWithEntities VoteRestrictionToastText( VoteRestrictionError error, not_null peer, @@ -144,16 +117,8 @@ template return tr::lng_polls_vote_restricted_subscribers_recent( tr::now, tr::rich); - case VoteRestrictionError::Countries: { - const auto countries = JoinCountryNames(poll->countries); - return countries.isEmpty() - ? tr::lng_polls_vote_restricted_countries(tr::now, tr::rich) - : tr::lng_polls_vote_restricted_countries_list( - tr::now, - lt_countries, - tr::bold(countries), - tr::rich); - } + case VoteRestrictionError::Countries: + return PollCountriesRestrictionText(poll->countries); case VoteRestrictionError::None: break; } diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 11b167240a..a5f37ac85a 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -8,11 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_poll.h" #include "api/api_text_entities.h" +#include "countries/countries_instance.h" #include "data/data_document.h" #include "data/data_photo.h" #include "data/data_user.h" #include "data/data_session.h" #include "base/call_delayed.h" +#include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/text/text_options.h" @@ -646,3 +648,40 @@ MTPInputMedia PollDataToInputMedia( ? PollMediaToMTP(poll->solutionMedia) : MTPInputMedia()); } + +QString JoinPollCountries(const std::vector &countriesIso2) { + auto countries = QStringList(); + countries.reserve(int(countriesIso2.size())); + const auto &instance = Countries::Instance(); + for (const auto &iso2 : countriesIso2) { + const auto name = instance.countryNameByISO2(iso2); + countries.push_back(name.isEmpty() ? iso2 : name); + } + if (countries.empty()) { + return QString(); + } + auto result = countries.front(); + for (auto i = 1, count = int(countries.size()); i != count; ++i) { + result = ((i + 1 == count) + ? tr::lng_prizes_countries_and_last + : tr::lng_prizes_countries_and_one)( + tr::now, + lt_countries, + result, + lt_country, + countries[i]); + } + return result; +} + +TextWithEntities PollCountriesRestrictionText( + const std::vector &countries) { + const auto joined = JoinPollCountries(countries); + return joined.isEmpty() + ? tr::lng_polls_vote_restricted_countries(tr::now, tr::rich) + : tr::lng_polls_vote_restricted_countries_list( + tr::now, + lt_countries, + tr::bold(joined), + tr::rich); +} diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index be839ca0e1..80b68eea8a 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -141,6 +141,11 @@ inline constexpr auto kDefaultPollCreateFlags = PollData::Flag::PublicVotes | PollData::Flag::OpenAnswers | PollData::Flag::ShuffleAnswers; +[[nodiscard]] QString JoinPollCountries( + const std::vector &countriesIso2); +[[nodiscard]] TextWithEntities PollCountriesRestrictionText( + const std::vector &countries); + [[nodiscard]] QByteArray PollOptionFromLink(const QString &value); [[nodiscard]] QString PollOptionToLink(const QByteArray &option); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 44f03da246..b9fa7d6222 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -3444,8 +3444,14 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (leaderOrSelf && !_menu->empty()) { const auto media = leaderOrSelf->media(); const auto poll = media ? media->poll() : nullptr; - if (poll && !poll->closed() && poll->hideResultsUntilClose()) { - HistoryView::InsertPollHiddenResultsLabel(_menu.get()); + if (poll && !poll->closed()) { + if (poll->hideResultsUntilClose()) { + HistoryView::InsertPollHiddenResultsLabel(_menu.get()); + } + HistoryView::InsertPollVoteRestrictionsLabel( + _menu.get(), + leaderOrSelf, + poll); } } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 234e7e7bf8..8d26f0dacb 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1310,24 +1310,76 @@ rpl::producer VoiceTimecodeUpdates(FullMsgId itemId) { }) | rpl::distinct_until_changed(); } -void InsertPollHiddenResultsLabel(not_null menu) { +void InsertPollMenuLabel( + not_null menu, + TextWithEntities text, + const style::MenuSeparator &separatorSt) { auto label = base::make_unique_q( menu->menu(), menu->st().menu, st::historyHasCustomEmoji, st::historyHasCustomEmojiPosition, - tr::lng_polls_context_ends(tr::now, tr::rich)); + std::move(text)); + label->setAttribute(Qt::WA_TransparentForMouseEvents); menu->insertAction(0, std::move(label)); const auto sepAction = new QAction(menu->menu()); sepAction->setSeparator(true); auto separator = base::make_unique_q( menu->menu(), menu->st().menu, - menu->st().menu.separator, + separatorSt, sepAction); menu->insertAction(1, std::move(separator)); } +void InsertPollHiddenResultsLabel(not_null menu) { + InsertPollMenuLabel( + menu, + tr::lng_polls_context_ends(tr::now, tr::rich), + menu->st().menu.separator); +} + +[[nodiscard]] TextWithEntities PollVoteRestrictionsLabelText( + not_null item, + not_null poll) { + auto result = TextWithEntities(); + if (poll->subscribersOnly()) { + const auto peer = item->history()->peer.get(); + const auto channel = peer->isBroadcast() + ? peer->name() + : QString(); + result = channel.isEmpty() + ? tr::lng_polls_vote_restricted_subscribers_recent( + tr::now, + tr::rich) + : tr::lng_polls_vote_restricted_subscribers_channel( + tr::now, + lt_channel, + tr::bold(channel), + tr::rich); + } + if (!poll->countries.empty()) { + auto countriesText = PollCountriesRestrictionText(poll->countries); + if (result.text.isEmpty()) { + result = std::move(countriesText); + } else { + result.append('\n').append(std::move(countriesText)); + } + } + return result; +} + +void InsertPollVoteRestrictionsLabel( + not_null menu, + not_null item, + not_null poll) { + auto text = PollVoteRestrictionsLabelText(item, poll); + if (text.text.isEmpty()) { + return; + } + InsertPollMenuLabel(menu, std::move(text), st::expandedMenuSeparator); +} + ContextMenuRequest::ContextMenuRequest( not_null navigation) : navigation(navigation) { @@ -1528,8 +1580,11 @@ base::unique_qptr FillContextMenu( if (item) { const auto media = item->media(); const auto poll = media ? media->poll() : nullptr; - if (poll && !poll->closed() && poll->hideResultsUntilClose()) { - InsertPollHiddenResultsLabel(result.get()); + if (poll && !poll->closed()) { + if (poll->hideResultsUntilClose()) { + InsertPollHiddenResultsLabel(result.get()); + } + InsertPollVoteRestrictionsLabel(result.get(), item, poll); } } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index ab022ef583..308f5e5685 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -59,6 +59,10 @@ base::unique_qptr FillContextMenu( const ContextMenuRequest &request); void InsertPollHiddenResultsLabel(not_null menu); +void InsertPollVoteRestrictionsLabel( + not_null menu, + not_null item, + not_null poll); void CopyPostLink( not_null controller, From 2359bd9844db1417ff95664e493492326e850f90 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 10 Apr 2026 18:10:51 +0300 Subject: [PATCH 083/154] [poll-view] Improved handle of poll vote restriction. --- Telegram/SourceFiles/api/api_polls.cpp | 67 ++++-------- Telegram/SourceFiles/data/data_poll.cpp | 45 ++++++++ Telegram/SourceFiles/data/data_poll.h | 15 +++ .../history/view/media/history_view_poll.cpp | 100 +++++++++++++++++- .../history/view/media/history_view_poll.h | 3 + 5 files changed, 181 insertions(+), 49 deletions(-) diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index a713643893..e451c829f2 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -34,13 +34,6 @@ namespace { constexpr auto kVoteRestrictionToastDuration = 5 * crl::time(1000); -enum class VoteRestrictionError { - None, - SubscribersOnly, - SubscribersJoinedTooRecently, - Countries, -}; - const auto kSubscribersOnlyVoteErrorPatterns = std::array{ u"POLL_SUBSCRIBERS_ONLY"_q, u"POLL_MEMBER_RESTRICTED"_q, @@ -80,60 +73,32 @@ template return false; } -[[nodiscard]] VoteRestrictionError ParseVoteRestrictionError( +[[nodiscard]] PollData::VoteRestriction ParseVoteRestrictionError( const QString &type) { if (MatchesErrorPattern( type, kSubscribersJoinedTooRecentlyVoteErrorPatterns)) { - return VoteRestrictionError::SubscribersJoinedTooRecently; + return PollData::VoteRestriction::SubscribersJoinedTooRecently; } else if (MatchesErrorPattern( type, kSubscribersOnlyVoteErrorPatterns)) { - return VoteRestrictionError::SubscribersOnly; + return PollData::VoteRestriction::SubscribersOnly; } else if (MatchesErrorPattern( type, kCountriesVoteErrorPatterns)) { - return VoteRestrictionError::Countries; + return PollData::VoteRestriction::Countries; } - return VoteRestrictionError::None; -} - -[[nodiscard]] TextWithEntities VoteRestrictionToastText( - VoteRestrictionError error, - not_null peer, - not_null poll) { - switch (error) { - case VoteRestrictionError::SubscribersOnly: { - const auto channel = peer->name(); - return channel.isEmpty() - ? tr::lng_polls_vote_restricted_subscribers(tr::now, tr::rich) - : tr::lng_polls_vote_restricted_subscribers_channel( - tr::now, - lt_channel, - tr::bold(channel), - tr::rich); - } - case VoteRestrictionError::SubscribersJoinedTooRecently: - return tr::lng_polls_vote_restricted_subscribers_recent( - tr::now, - tr::rich); - case VoteRestrictionError::Countries: - return PollCountriesRestrictionText(poll->countries); - case VoteRestrictionError::None: - break; - } - return {}; + return PollData::VoteRestriction::None; } void ShowVoteRestrictionToast( not_null peer, not_null poll, - const MTP::Error &error) { - const auto parsed = ParseVoteRestrictionError(error.type()); - if (parsed == VoteRestrictionError::None) { + PollData::VoteRestriction restriction) { + if (restriction == PollData::VoteRestriction::None) { return; } - auto text = VoteRestrictionToastText(parsed, peer, poll); + auto text = PollVoteRestrictionText(restriction, peer, poll); if (text.text.isEmpty()) { return; } @@ -409,12 +374,26 @@ void Polls::sendVotes( )).done([=](const MTPUpdates &result) { _pollVotesRequestIds.erase(itemId); hideSending(); + if (poll) { + if (poll->voteRestriction() != PollData::VoteRestriction::None) { + poll->setVoteRestriction(PollData::VoteRestriction::None); + _session->data().notifyPollUpdateDelayed(poll); + } + } _session->updates().applyUpdates(result); }).fail([=](const MTP::Error &error) { _pollVotesRequestIds.erase(itemId); hideSending(); if (poll) { - ShowVoteRestrictionToast(peer, poll, error); + const auto restriction = ParseVoteRestrictionError(error.type()); + if (restriction != PollData::VoteRestriction::None) { + poll->setVoteRestriction(restriction); + _session->data().notifyPollUpdateDelayed(poll); + if (const auto item = _session->data().message(itemId)) { + _session->data().requestItemResize(item); + } + ShowVoteRestrictionToast(peer, poll, restriction); + } } }).send(); _pollVotesRequestIds.emplace(itemId, requestId); diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index a5f37ac85a..10d1340d6d 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -403,6 +403,24 @@ bool PollData::canViewStats() const { return (_flags & Flag::CanViewStats); } +void PollData::setVoteRestriction(VoteRestriction restriction) { + _voteRestrictionUpdated = (restriction == VoteRestriction::None) + ? 0 + : crl::now(); + if (_voteRestriction != restriction) { + _voteRestriction = restriction; + ++version; + } +} + +PollData::VoteRestriction PollData::voteRestriction() const { + return _voteRestriction; +} + +crl::time PollData::voteRestrictionUpdated() const { + return _voteRestrictionUpdated; +} + QString PollData::debugString() const { auto result = QString(); result += u"Poll #"_q + QString::number(id) + u'\n'; @@ -685,3 +703,30 @@ TextWithEntities PollCountriesRestrictionText( tr::bold(joined), tr::rich); } + +TextWithEntities PollVoteRestrictionText( + PollData::VoteRestriction restriction, + not_null peer, + not_null poll) { + switch (restriction) { + case PollData::VoteRestriction::SubscribersOnly: { + const auto channel = peer->name(); + return channel.isEmpty() + ? tr::lng_polls_vote_restricted_subscribers(tr::now, tr::rich) + : tr::lng_polls_vote_restricted_subscribers_channel( + tr::now, + lt_channel, + tr::bold(channel), + tr::rich); + } + case PollData::VoteRestriction::SubscribersJoinedTooRecently: + return tr::lng_polls_vote_restricted_subscribers_recent( + tr::now, + tr::rich); + case PollData::VoteRestriction::Countries: + return PollCountriesRestrictionText(poll->countries); + case PollData::VoteRestriction::None: + break; + } + return {}; +} diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 80b68eea8a..d7625de704 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -81,6 +81,12 @@ struct PollData { }; friend inline constexpr bool is_flag_type(Flag) { return true; }; using Flags = base::flags; + enum class VoteRestriction { + None, + SubscribersOnly, + SubscribersJoinedTooRecently, + Countries, + }; bool closeByTimer(); bool applyChanges(const MTPDpoll &poll); @@ -105,6 +111,9 @@ struct PollData { [[nodiscard]] bool creator() const; [[nodiscard]] bool subscribersOnly() const; [[nodiscard]] bool canViewStats() const; + void setVoteRestriction(VoteRestriction restriction); + [[nodiscard]] VoteRestriction voteRestriction() const; + [[nodiscard]] crl::time voteRestrictionUpdated() const; [[nodiscard]] QString debugString() const; @@ -132,6 +141,8 @@ private: const not_null _owner; Flags _flags = Flags(); + VoteRestriction _voteRestriction = VoteRestriction::None; + crl::time _voteRestrictionUpdated = 0; crl::time _lastResultsUpdate = 0; // < 0 means force reload. }; @@ -145,6 +156,10 @@ inline constexpr auto kDefaultPollCreateFlags = PollData::Flag::PublicVotes const std::vector &countriesIso2); [[nodiscard]] TextWithEntities PollCountriesRestrictionText( const std::vector &countries); +[[nodiscard]] TextWithEntities PollVoteRestrictionText( + PollData::VoteRestriction restriction, + not_null peer, + not_null poll); [[nodiscard]] QByteArray PollOptionFromLink(const QString &value); [[nodiscard]] QString PollOptionToLink(const QByteArray &option); diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 1a93fa2a3d..b179dedcb6 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_group_call_bar.h" #include "data/data_media_types.h" #include "data/data_document.h" +#include "data/data_channel.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_file_origin.h" @@ -78,6 +79,15 @@ constexpr auto kRotateAmplitude = 3.; constexpr auto kScaleSegments = 2; constexpr auto kScaleAmplitude = 0.03; constexpr auto kRollDuration = crl::time(400); +constexpr auto kExpiringVoteRestrictionDuration = 10 * 60 * crl::time(1000); +constexpr auto kVoteRestrictionToastDuration = 5 * crl::time(1000); + +[[nodiscard]] bool IsExpiringVoteRestriction( + PollData::VoteRestriction restriction) { + using Restriction = PollData::VoteRestriction; + return (restriction == Restriction::SubscribersOnly) + || (restriction == Restriction::SubscribersJoinedTooRecently); +} [[nodiscard]] int PollAnswerMediaSize() { return st::historyPollRadio.diameter * 2; @@ -1541,6 +1551,7 @@ struct Poll::Options : public Poll::Part { void saveStateInAnimation() const; void startAnswersAnimation() const; void toggleRipple(Answer &answer, bool pressed); + void clearSelected(); void toggleMultiOption(const QByteArray &option); void sendMultiOptions(); void showResults(); @@ -1898,6 +1909,54 @@ bool Poll::showVotes() const { return _voted || (_flags & PollData::Flag::Closed); } +PollData::VoteRestriction Poll::knownVoteRestriction() const { + const auto fromServer = _poll->voteRestriction(); + if (fromServer != PollData::VoteRestriction::None) { + if (IsExpiringVoteRestriction(fromServer)) { + const auto updated = _poll->voteRestrictionUpdated(); + if (updated > 0 + && (updated + kExpiringVoteRestrictionDuration <= crl::now())) { + return PollData::VoteRestriction::None; + } + } + return fromServer; + } + if (_poll->subscribersOnly()) { + const auto channel = _parent->data()->history()->peer->asChannel(); + if (channel && !channel->amIn()) { + return PollData::VoteRestriction::SubscribersOnly; + } + } + return PollData::VoteRestriction::None; +} + +bool Poll::voteRestricted() const { + return (knownVoteRestriction() != PollData::VoteRestriction::None) + && !showVotes() + && !_voted + && _parent->data()->isRegular(); +} + +void Poll::showVoteRestrictionToast() const { + const auto restriction = knownVoteRestriction(); + if (restriction == PollData::VoteRestriction::None) { + return; + } + const auto peer = _parent->data()->history()->peer; + auto text = PollVoteRestrictionText(restriction, peer, _poll); + if (text.text.isEmpty()) { + return; + } + if (const auto window = peer->session().tryResolveWindow(peer)) { + window->showToast({ + .text = std::move(text), + .iconLottie = u"ban"_q, + .iconLottieSize = st::pollToastIconSize, + .duration = kVoteRestrictionToastDuration, + }); + } +} + bool Poll::isAuthorNotVoted() const { return _parent->data()->out() && !_voted @@ -1905,7 +1964,10 @@ bool Poll::isAuthorNotVoted() const { } bool Poll::canVote() const { - return !showVotes() && !_voted && _parent->data()->isRegular(); + return !showVotes() + && !_voted + && _parent->data()->isRegular() + && (knownVoteRestriction() == PollData::VoteRestriction::None); } bool Poll::canSendVotes() const { @@ -1913,6 +1975,9 @@ bool Poll::canSendVotes() const { } bool Poll::showVotersCount() const { + if (voteRestricted()) { + return true; + } if (_voted && !showVotes()) { return !(_flags & PollData::Flag::PublicVotes); } @@ -2706,6 +2771,8 @@ ClickHandlerPtr Poll::Options::createAnswerClickHandler( } if (_owner->canVote()) { _owner->_optionsPart->toggleMultiOption(option); + } else if (_owner->voteRestricted()) { + _owner->showVoteRestrictionToast(); } else if (_owner->showVotes()) { _owner->_optionsPart->showAnswerVotesTooltip(option); } @@ -2722,6 +2789,8 @@ ClickHandlerPtr Poll::Options::createAnswerClickHandler( _owner->history()->session().api().polls().sendVotes( _owner->_parent->data()->fullId(), { option }); + } else if (_owner->voteRestricted()) { + _owner->showVoteRestrictionToast(); } else if (_owner->showVotes()) { _owner->_optionsPart->showAnswerVotesTooltip(option); } @@ -2757,6 +2826,26 @@ void Poll::Options::toggleMultiOption(const QByteArray &option) { } } +void Poll::Options::clearSelected() { + auto changed = false; + for (auto &answer : _answers) { + if (answer.selected) { + answer.selected = false; + changed = true; + } + if (answer.selectedAnimation.animating()) { + answer.selectedAnimation.stop(); + } + } + if (_hasSelected) { + _hasSelected = false; + changed = true; + } + if (changed) { + _owner->repaint(); + } +} + void Poll::Options::sendMultiOptions() { auto chosen = _answers | ranges::views::filter( &Answer::selected @@ -2801,9 +2890,7 @@ void Poll::updateVotes() { if (_voted != voted) { _voted = voted; if (_voted) { - for (auto &answer : _optionsPart->_answers) { - answer.selected = false; - } + _optionsPart->clearSelected(); if (_optionsPart->_votedFromHere && (_flags & PollData::Flag::HideResultsUntilClose) && !(_flags & PollData::Flag::Closed)) { @@ -2818,9 +2905,12 @@ void Poll::updateVotes() { } } else { _optionsPart->_votedFromHere = false; - _optionsPart->_hasSelected = false; + _optionsPart->clearSelected(); } } + if (voteRestricted()) { + _optionsPart->clearSelected(); + } _optionsPart->updateAnswerVotes(); _footerPart->updateTotalVotes(); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.h b/Telegram/SourceFiles/history/view/media/history_view_poll.h index bdf006163a..f5a6e36cc6 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.h +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.h @@ -95,6 +95,9 @@ private: QSize countCurrentSize(int newWidth) override; [[nodiscard]] bool showVotes() const; + [[nodiscard]] PollData::VoteRestriction knownVoteRestriction() const; + [[nodiscard]] bool voteRestricted() const; + void showVoteRestrictionToast() const; [[nodiscard]] bool canVote() const; [[nodiscard]] bool canSendVotes() const; [[nodiscard]] bool isAuthorNotVoted() const; From 9636dbe71a03c09e720d6f4fd428b0e0b4edcc17 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 11 Apr 2026 14:08:30 +0700 Subject: [PATCH 084/154] Update API scheme on layer 225. --- Telegram/SourceFiles/api/api_updates.cpp | 2 ++ .../SourceFiles/data/business/data_shortcut_messages.cpp | 1 + Telegram/SourceFiles/data/components/scheduled_messages.cpp | 2 ++ Telegram/SourceFiles/data/data_session.cpp | 1 + .../history/admin_log/history_admin_log_item.cpp | 1 + Telegram/SourceFiles/mtproto/scheme/api.tl | 6 ++++-- .../SourceFiles/settings/settings_privacy_controllers.cpp | 1 + 7 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 1aa8fc9df8..f292b623aa 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1209,6 +1209,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), MTP_long(d.vvia_bot_id().value_or_empty()), MTPlong(), // via_business_bot_id + MTPPeer(), // guestchat_via_from d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(), d.vdate(), d.vmessage(), @@ -1252,6 +1253,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), MTP_long(d.vvia_bot_id().value_or_empty()), MTPlong(), // via_business_bot_id + MTPPeer(), // guestchat_via_from d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(), d.vdate(), d.vmessage(), diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index 13c74ef7a1..11cf73de31 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -73,6 +73,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), MTP_long(data.vvia_bot_id().value_or_empty()), MTP_long(data.vvia_business_bot_id().value_or_empty()), + data.vguestchat_via_from() ? *data.vguestchat_via_from() : MTPPeer(), data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), data.vdate(), data.vmessage(), diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index 9fc3b15026..2076654a42 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -77,6 +77,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), MTP_long(data.vvia_bot_id().value_or_empty()), MTP_long(data.vvia_business_bot_id().value_or_empty()), + data.vguestchat_via_from() ? *data.vguestchat_via_from() : MTPPeer(), data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), data.vdate(), data.vmessage(), @@ -258,6 +259,7 @@ void ScheduledMessages::sendNowSimpleMessage( MTPMessageFwdHeader(), MTPlong(), // via_bot_id MTPlong(), // via_business_bot_id + MTPPeer(), // guestchat_via_from replyHeader, update.vdate(), MTP_string(local->originalText().text), diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 3822e7c187..89d145c0bd 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -5319,6 +5319,7 @@ void Session::insertCheckedServiceNotification( MTPMessageFwdHeader(), MTPlong(), // via_bot_id MTPlong(), // via_business_bot_id + MTPPeer(), // guestchat_via_from MTPMessageReplyHeader(), MTP_int(date), MTP_string(sending.text), diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 4cb98ed698..81e2e577d5 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -179,6 +179,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), MTP_long(data.vvia_bot_id().value_or_empty()), MTP_long(data.vvia_business_bot_id().value_or_empty()), + data.vguestchat_via_from() ? *data.vguestchat_via_from() : MTPPeer(), reply.value_or(MTPMessageReplyHeader()), MTP_int(newDate), data.vmessage(), diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 0caaab2f5f..7420eaa597 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -86,7 +86,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#31774388 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true bot_forum_view:flags2.16?true bot_forum_can_manage_topics:flags2.17?true bot_can_manage_bots:flags2.18?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User; +user#31774388 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true bot_forum_view:flags2.16?true bot_forum_can_manage_topics:flags2.17?true bot_can_manage_bots:flags2.18?true bot_guestchat:flags2.19?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -118,7 +118,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#3ae56482 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int from_rank:flags2.12?string peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = Message; +message#95ef6f2b flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int from_rank:flags2.12?string peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long guestchat_via_from:flags2.19?Peer reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = Message; messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -465,6 +465,7 @@ updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update; updateStarGiftCraftFail#ac072444 = Update; updateChatParticipantRank#bd8367b9 chat_id:long user_id:long rank:string version:int = Update; updateManagedBot#4880ed9a user_id:long bot_id:long qts:int = Update; +updateBotGuestChatQuery#cdd4093d flags:# query_id:long message:Message reference_messages:flags.0?Vector qts:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -2610,6 +2611,7 @@ messages.addPollAnswer#19bc4b6d peer:InputPeer msg_id:int answer:PollAnswer = Up messages.deletePollAnswer#ac8505a5 peer:InputPeer msg_id:int option:bytes = Updates; messages.getUnreadPollVotes#43286cf2 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readPollVotes#1720b4d8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.setBotGuestChatResult#52b08db query_id:long result:InputBotInlineResult = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 1737f27d81..02c801f7d5 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -186,6 +186,7 @@ AdminLog::OwnedItem GenerateForwardedItem( MTPstring()), // psa_type MTPlong(), // via_bot_id MTPlong(), // via_business_bot_id + MTPPeer(), // guestchat_via_from MTPMessageReplyHeader(), MTP_int(base::unixtime::now()), // date MTP_string(text), From ffe5f1ac00bf18ff668aab9e98fcfa2670533b52 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 11 Apr 2026 16:16:53 +0700 Subject: [PATCH 085/154] Support guest chat bot messages. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/data/data_session.cpp | 1 + Telegram/SourceFiles/data/data_types.h | 2 + Telegram/SourceFiles/data/data_user.h | 1 + Telegram/SourceFiles/history/history_item.cpp | 16 ++++- Telegram/SourceFiles/history/history_item.h | 1 + .../history/history_item_components.cpp | 36 ++++++++++ .../history/history_item_components.h | 12 ++++ .../history/history_item_helpers.cpp | 3 + .../history/view/history_view_element.cpp | 1 + .../history/view/history_view_message.cpp | 70 ++++++++++++++++++- 11 files changed, 142 insertions(+), 2 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 55555727c7..cd42bc0994 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4455,6 +4455,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_inline_bot_no_results" = "No results."; "lng_inline_bot_via" = "via {inline_bot}"; +"lng_guest_chat_for" = "for {user}"; "lng_box_remove" = "Remove"; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 89d145c0bd..c5313fb154 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -591,6 +591,7 @@ not_null Session::processUser(const MTPUser &data) { info->hasMainApp = data.is_bot_has_main_app(); info->userCreatesTopics = data.is_bot_forum_can_manage_topics(); info->canManageBots = data.is_bot_can_manage_bots(); + info->supportsGuestChat = data.is_bot_guestchat(); } } diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 54381b6330..43d1f4faff 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -367,6 +367,8 @@ enum class MessageFlag : uint64 { TextAppearing = (1ULL << 60), TextAppearingStarted = (1ULL << 61), + + GuestChatViaFrom = (1ULL << 62), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index e1894fcd26..8eb1076614 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -108,6 +108,7 @@ struct BotInfo { bool userCreatesTopics : 1 = false; bool setBotPhotoHidden : 1 = false; bool canManageBots : 1 = false; + bool supportsGuestChat : 1 = false; private: std::unique_ptr _forum; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 88c7bba933..fd01503198 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -168,6 +168,7 @@ struct HistoryItem::CreateConfig { UserId viaBotId = 0; UserId viaBusinessBotId = 0; + PeerId guestChatViaFrom = 0; int viewsCount = -1; int forwardsCount = -1; int boostsApplied = 0; @@ -1917,6 +1918,10 @@ UserData *HistoryItem::viaBot() const { return nullptr; } +bool HistoryItem::isGuestChatBotMessage() const { + return (_flags & MessageFlag::GuestChatViaFrom); +} + UserData *HistoryItem::getMessageBot() const { if (const auto bot = viaBot()) { return bot; @@ -4203,7 +4208,7 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const { const auto sender = [&]() -> std::optional { if (options.hideSender || isPostHidingAuthor() || isEmpty()) { return {}; - } else if (!_history->peer->isUser()) { + } else if (!_history->peer->isUser() || isGuestChatBotMessage()) { if (const auto from = displayFrom()) { return fromSender(from); } @@ -4260,6 +4265,9 @@ void HistoryItem::createComponents(CreateConfig &&config) { if (config.viaBotId) { mask |= HistoryMessageVia::Bit(); } + if (config.guestChatViaFrom) { + mask |= HistoryMessageGuestChat::Bit(); + } if (config.viewsCount >= 0 || !config.replies.isNull) { mask |= HistoryMessageViews::Bit(); } @@ -4354,6 +4362,9 @@ void HistoryItem::createComponents(CreateConfig &&config) { if (const auto via = Get()) { via->create(&_history->owner(), config.viaBotId); } + if (const auto guestChat = Get()) { + guestChat->create(&_history->owner(), config.guestChatViaFrom); + } if (Has()) { changeViewsCount(config.viewsCount); if (config.replies.isNull @@ -4737,6 +4748,9 @@ void HistoryItem::createComponents(const MTPDmessage &data) { } config.viaBotId = data.vvia_bot_id().value_or_empty(); config.viaBusinessBotId = data.vvia_business_bot_id().value_or_empty(); + config.guestChatViaFrom = data.vguestchat_via_from() + ? peerFromMTP(*data.vguestchat_via_from()) + : PeerId(); config.viewsCount = data.vviews().value_or(-1); config.forwardsCount = data.vforwards().value_or(-1); config.replies = isScheduled() diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index dd189bccef..1da3599ef8 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -193,6 +193,7 @@ public: void updateStoryMentionText(); [[nodiscard]] UserData *viaBot() const; + [[nodiscard]] bool isGuestChatBotMessage() const; [[nodiscard]] UserData *getMessageBot() const; [[nodiscard]] bool hideLinks() const; [[nodiscard]] bool isHistoryEntry() const; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 47007d9c39..b77cf37ecb 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -163,6 +163,42 @@ void HistoryMessageVia::resize(int32 availw) const { } } +void HistoryMessageGuestChat::create( + not_null owner, + PeerId visitorId) { + visitor = owner->peer(visitorId); + const auto firstName = visitor->isUser() + ? visitor->asUser()->firstName + : visitor->name(); + maxWidth = st::msgServiceNameFont->width( + tr::lng_guest_chat_for(tr::now, lt_user, firstName)); + link = std::make_shared([peer = this->visitor]( + ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + controller->showPeerInfo(peer); + } + }); +} + +void HistoryMessageGuestChat::resize(int32 availw) const { + if (availw < 0) { + text = QString(); + width = 0; + } else { + const auto firstName = visitor->isUser() + ? visitor->asUser()->firstName + : visitor->name(); + text = tr::lng_guest_chat_for(tr::now, lt_user, firstName); + if (availw < maxWidth) { + text = st::msgServiceNameFont->elided(text, availw); + width = st::msgServiceNameFont->width(text); + } else if (width < maxWidth) { + width = maxWidth; + } + } +} + HiddenSenderInfo::HiddenSenderInfo( const QString &name, bool external, diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 39fd5058e1..9dcd817b26 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -78,6 +78,18 @@ struct HistoryMessageVia : RuntimeComponent { ClickHandlerPtr link; }; +struct HistoryMessageGuestChat +: RuntimeComponent { + void create(not_null owner, PeerId visitorId); + void resize(int32 availw) const; + + PeerData *visitor = nullptr; + mutable QString text; + mutable int width = 0; + mutable int maxWidth = 0; + ClickHandlerPtr link; +}; + struct HistoryMessageViews : RuntimeComponent { static constexpr auto kMaxRecentRepliers = 3; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 5068d3aaac..11dd7b40dd 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -859,6 +859,9 @@ MessageFlags FlagsFromMTP( : Flag()) | ((flags & MTP::f_summary_from_language) ? Flag::CanBeSummarized + : Flag()) + | ((flags & MTP::f_guestchat_via_from) + ? Flag::GuestChatViaFrom : Flag()); } diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index b94296a470..d1a6a18d43 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1973,6 +1973,7 @@ bool Element::computeIsAttachToPrevious(not_null previous) { return !item->isService() && !item->isEmpty() && !item->isPostHidingAuthor() + && !item->isGuestChatBotMessage() && (!item->history()->peer->isMegagroup() || !view->hasOutLayout() || !item->from()->isChannel()); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 7027e88378..c6415941c6 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -891,6 +891,9 @@ QSize Message::performCountOptimalSize() { namew += st::msgServiceFont->spacew + via->maxWidth + (_fromNameStatus ? st::msgServiceFont->spacew : 0); } + if (const auto guestChat = item->Get()) { + namew += st::msgServiceFont->spacew + guestChat->maxWidth; + } if (Has()) { namew += st::msgPadding.right() + rightBadgeWidth(); } @@ -1904,6 +1907,20 @@ void Message::paintFromName( availableLeft += skipWidth; availableWidth -= skipWidth; } + if (const auto guestChat = item->Get()) { + if (availableWidth > 0) { + p.setPen(stm->msgServiceFg); + paintLinkRipple( + p, + guestChat->link, + QRect(availableLeft, trect.top(), guestChat->width, st::msgServiceFont->height), + trect.topLeft()); + p.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, guestChat->text); + auto skipWidth = guestChat->width + st::msgServiceFont->spacew; + availableLeft += skipWidth; + availableWidth -= skipWidth; + } + } if (badgeWidth) { p.setPen(stm->msgDateFg); if (const auto badge = Get()) { @@ -2927,6 +2944,9 @@ bool Message::hasFromPhoto() const { return !hasOutLayout(); } } + if (item->isGuestChatBotMessage()) { + return true; + } return !item->out() && !item->history()->peer->isUser(); } break; case Context::ContactPreview: @@ -3325,6 +3345,24 @@ bool Message::getStateFromName( recordLinkRipplePoint(point, trect.topLeft()); return true; } + if (const auto guestChat = item->Get()) { + auto guestChatLeft = availableLeft + nameText->maxWidth() + + st::msgServiceFont->spacew; + if (via && !displayForwardedFrom()) { + guestChatLeft += via->width + st::msgServiceFont->spacew; + } + if (_fromNameStatus) { + guestChatLeft += st::dialogsPremiumIcon.icon.width() + + st::msgServiceFont->spacew; + } + if (point.x() >= guestChatLeft + && point.x() < availableLeft + availableWidth + && point.x() < guestChatLeft + guestChat->width) { + outResult->link = guestChat->link; + recordLinkRipplePoint(point, trect.topLeft()); + return true; + } + } if (badgeWidth) { const auto badge = Get(); const auto badgeLeft = trect.left() @@ -4338,7 +4376,7 @@ bool Message::hasFromName() const { } } return false; - } else if (!peer->isUser()) { + } else if (!peer->isUser() || item->isGuestChatBotMessage()) { if (const auto media = this->media()) { return !media->hideFromName(); } @@ -4416,6 +4454,9 @@ bool Message::hasOutLayout() const { } } } + if (item->isGuestChatBotMessage()) { + return false; + } return item->out() && !item->isPost(); } @@ -4922,6 +4963,33 @@ void Message::fromNameUpdated(int width) const { - st::msgServiceFont->spacew); } } + if (const auto guestChat = item->Get()) { + const auto nameText = [&]() -> const Ui::Text::String * { + if (from) { + return &_fromName; + } else if (const auto info = item->originalHiddenSenderInfo()) { + return &info->nameText(); + } else { + Unexpected("Corrupted forwarded information in message."); + } + }(); + auto viaWidth = 0; + if (const auto via = item->Get()) { + if (!displayForwardedFrom()) { + viaWidth = st::msgServiceFont->spacew + via->width; + } + } + guestChat->resize(width + - st::msgPadding.left() + - st::msgPadding.right() + - nameText->maxWidth() + - (_fromNameStatus + ? (st::dialogsPremiumIcon.icon.width() + + st::msgServiceFont->spacew) + : 0) + - st::msgServiceFont->spacew + - viaWidth); + } } TextSelection Message::skipTextSelection(TextSelection selection) const { From a65593caddfaee3c580c84efbf6ac06111aa31eb Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 12 Apr 2026 11:44:50 +0700 Subject: [PATCH 086/154] Fix userpic painting for guest chat bot messages. --- Telegram/SourceFiles/history/history.cpp | 8 ++++++++ Telegram/SourceFiles/history/history.h | 3 +++ Telegram/SourceFiles/history/history_inner_widget.cpp | 3 ++- Telegram/SourceFiles/history/history_item.cpp | 3 +++ .../SourceFiles/history/view/history_view_message.cpp | 4 ++++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 04ad57bc2b..03ce20f477 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2761,6 +2761,14 @@ bool History::loadedAtTop() const { return _loadedAtTop; } +bool History::hasGuestChatBotMessages() const { + return _flags & Flag::HasGuestChatBotMessages; +} + +void History::setHasGuestChatBotMessages() { + _flags |= Flag::HasGuestChatBotMessages; +} + bool History::isReadyFor(MsgId msgId) { if (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) { // Old group history. diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index f85d8daa21..9c561e79d1 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -242,6 +242,8 @@ public: [[nodiscard]] bool loadedAtBottom() const; // last message is in the list void setNotLoadedAtBottom(); [[nodiscard]] bool loadedAtTop() const; // nothing was added after loading history back + [[nodiscard]] bool hasGuestChatBotMessages() const; + void setHasGuestChatBotMessages(); [[nodiscard]] bool isReadyFor(MsgId msgId); // has messages for showing history at msgId void getReadyFor(MsgId msgId); @@ -499,6 +501,7 @@ private: HasPinnedMessages = (1 << 6), ResolveChatListMessage = (1 << 7), MonoAndForumUnreadInvalidatePending = (1 << 8), + HasGuestChatBotMessages = (1 << 9), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index b9fa7d6222..858ef7487c 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -912,7 +912,8 @@ bool HistoryInner::canHaveFromUserpics() const { && !_peer->isSelf() && !_peer->isRepliesChat() && !_peer->isVerifyCodes() - && !_isChatWide) { + && !_isChatWide + && !_history->hasGuestChatBotMessages()) { return false; } else if (const auto channel = _peer->asBroadcast()) { return channel->signatureProfiles(); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index fd01503198..115b52201f 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -813,6 +813,9 @@ HistoryItem::HistoryItem( if (_effectId) { _history->owner().reactions().preloadEffectImageFor(_effectId); } + if (isGuestChatBotMessage()) { + _history->setHasGuestChatBotMessages(); + } } HistoryItem::HistoryItem( diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index c6415941c6..705befeac1 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2538,6 +2538,10 @@ void Message::clickHandlerPressedChanged( && (handler == via->link) && !displayForwardedFrom()) { startLinkRipple(); + } else if (const auto guestChat = data()->Get() + ; guestChat + && (handler == guestChat->link)) { + startLinkRipple(); } else if (const auto forwarded = data()->Get() ; forwarded && displayForwardedFrom() From 218ca1793dab2336baa5338c8ddc7d10a0053659 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 13 Apr 2026 22:52:00 +0700 Subject: [PATCH 087/154] Add an icon for new tone creation. --- Telegram/Resources/icons/menu/edit_stars_add.svg | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Telegram/Resources/icons/menu/edit_stars_add.svg diff --git a/Telegram/Resources/icons/menu/edit_stars_add.svg b/Telegram/Resources/icons/menu/edit_stars_add.svg new file mode 100644 index 0000000000..04b6a8c915 --- /dev/null +++ b/Telegram/Resources/icons/menu/edit_stars_add.svg @@ -0,0 +1,10 @@ + + + + + + + + + + From b01a265199dbb4dc9bd9f2fc3664ef45726d0eb7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 13 Apr 2026 22:53:12 +0700 Subject: [PATCH 088/154] Update API on layer 225. --- Telegram/SourceFiles/mtproto/scheme/api.tl | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 7420eaa597..192259589a 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -466,6 +466,7 @@ updateStarGiftCraftFail#ac072444 = Update; updateChatParticipantRank#bd8367b9 chat_id:long user_id:long rank:string version:int = Update; updateManagedBot#4880ed9a user_id:long bot_id:long qts:int = Update; updateBotGuestChatQuery#cdd4093d flags:# query_id:long message:Message reference_messages:flags.0?Vector qts:int = Update; +updateAiComposeTones#8c0f91fb = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -2158,6 +2159,16 @@ channelTopic#93a5df73 id:int title:string = ChannelTopic; stats.pollStats#2999beed votes_graph:StatsGraph = stats.PollStats; +inputAiComposeToneDefault#1fe9a9bf tone:string = InputAiComposeTone; +inputAiComposeToneID#773c080 id:long access_hash:long = InputAiComposeTone; +inputAiComposeToneSlug#1fa01357 slug:string = InputAiComposeTone; + +aiComposeTone#12ea1465 flags:# creator:flags.0?true id:long access_hash:long slug:string title:string emoji_id:flags.1?long prompt:string installs_count:flags.2?int author_id:flags.3?long = AiComposeTone; +aiComposeToneDefault#9bad6414 tone:string emoji_id:long title:string = AiComposeTone; + +aicompose.tonesNotModified#c1f46103 = aicompose.Tones; +aicompose.tones#65175942 hash:long tones:Vector = aicompose.Tones; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2604,7 +2615,7 @@ messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User; messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates; messages.declineUrlAuth#35436bbc url:string = Bool; messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool; -messages.composeMessageWithAI#fd426afe flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string change_tone:flags.2?string = messages.ComposedMessageWithAI; +messages.composeMessageWithAI#daecc589 flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string tone:flags.2?InputAiComposeTone = messages.ComposedMessageWithAI; messages.reportReadMetrics#4067c5e6 peer:InputPeer metrics:Vector = Bool; messages.reportMusicListen#ddbcd819 id:InputDocument listened_duration:int = Bool; messages.addPollAnswer#19bc4b6d peer:InputPeer msg_id:int answer:PollAnswer = Updates; @@ -2957,4 +2968,11 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; +aicompose.createTone#70f76f8 flags:# display_author:flags.0?true emoji_id:flags.1?long title:string prompt:string = AiComposeTone; +aicompose.updateTone#903bcf59 flags:# tone:InputAiComposeTone display_author:flags.0?Bool emoji_id:flags.1?long title:flags.2?string prompt:flags.3?string = AiComposeTone; +aicompose.saveTone#1782cbb1 tone:InputAiComposeTone unsave:Bool = Bool; +aicompose.deleteTone#dd39316a tone:InputAiComposeTone = Bool; +aicompose.getTone#4679e1df tone:InputAiComposeTone = AiComposeTone; +aicompose.getTones#abd59201 hash:long = aicompose.Tones; + // LAYER 225 From 3cc2192791a49e76f75b93da952027574f27b067 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 14 Apr 2026 10:40:49 +0700 Subject: [PATCH 089/154] Initial cloud tones support. --- Telegram/CMakeLists.txt | 4 + Telegram/Resources/langs/lang.strings | 13 + .../SourceFiles/api/api_compose_with_ai.cpp | 15 +- .../SourceFiles/api/api_compose_with_ai.h | 15 +- Telegram/SourceFiles/api/api_updates.cpp | 5 + Telegram/SourceFiles/boxes/boxes.style | 3 + Telegram/SourceFiles/boxes/compose_ai_box.cpp | 128 +++++++++- .../SourceFiles/boxes/create_ai_tone_box.cpp | 129 ++++++++++ .../SourceFiles/boxes/create_ai_tone_box.h | 31 +++ .../SourceFiles/core/local_url_handlers.cpp | 49 ++++ .../data/data_ai_compose_tones.cpp | 225 ++++++++++++++++++ .../SourceFiles/data/data_ai_compose_tones.h | 90 +++++++ Telegram/SourceFiles/data/data_poll.cpp | 2 +- Telegram/SourceFiles/data/data_session.cpp | 2 + Telegram/SourceFiles/data/data_session.h | 5 + .../ui/controls/compose_ai_button_factory.cpp | 5 +- .../ui/controls/labeled_emoji_tabs.cpp | 41 +++- .../ui/controls/labeled_emoji_tabs.h | 5 + 18 files changed, 748 insertions(+), 19 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/create_ai_tone_box.cpp create mode 100644 Telegram/SourceFiles/boxes/create_ai_tone_box.h create mode 100644 Telegram/SourceFiles/data/data_ai_compose_tones.cpp create mode 100644 Telegram/SourceFiles/data/data_ai_compose_tones.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1c7484a603..fdde7123e2 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -386,6 +386,8 @@ PRIVATE boxes/transfer_gift_box.h boxes/compose_ai_box.cpp boxes/compose_ai_box.h + boxes/create_ai_tone_box.cpp + boxes/create_ai_tone_box.h boxes/translate_box.cpp boxes/translate_box.h boxes/url_auth_box.cpp @@ -611,6 +613,8 @@ PRIVATE data/data_abstract_sparse_ids.h data/data_abstract_structure.cpp data/data_abstract_structure.h + data/data_ai_compose_tones.cpp + data/data_ai_compose_tones.h data/data_audio_msg_id.cpp data/data_audio_msg_id.h data/data_auto_download.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index cd42bc0994..9971741fdb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7919,6 +7919,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_select_style" = "Select Style"; "lng_ai_compose_apply_style" = "Apply Style"; "lng_ai_compose_style_tooltip" = "Choose Style"; +"lng_ai_compose_add_style" = "Add Style"; +"lng_ai_compose_create_tone_title" = "New Style"; +"lng_ai_compose_edit_tone_title" = "Edit Style"; +"lng_ai_compose_tone_name" = "Name"; +"lng_ai_compose_tone_prompt" = "Prompt"; +"lng_ai_compose_tone_save" = "Save"; +"lng_ai_compose_tone_edit" = "Edit"; +"lng_ai_compose_tone_share" = "Share"; +"lng_ai_compose_tone_delete" = "Delete"; +"lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style?"; +"lng_ai_compose_tone_save_sure" = "Do you want to save the style {title}?"; +"lng_ai_compose_tone_saved" = "Style saved."; +"lng_ai_compose_tone_link_copied" = "Style link copied."; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/SourceFiles/api/api_compose_with_ai.cpp b/Telegram/SourceFiles/api/api_compose_with_ai.cpp index dfebd67e5f..a6a474e762 100644 --- a/Telegram/SourceFiles/api/api_compose_with_ai.cpp +++ b/Telegram/SourceFiles/api/api_compose_with_ai.cpp @@ -40,8 +40,8 @@ mtpRequestId ComposeWithAi::request( if (!request.translateToLang.isEmpty()) { flags |= Flag::f_translate_to_lang; } - if (!request.changeTone.isEmpty()) { - flags |= Flag::f_change_tone; + if (request.tone) { + flags |= Flag::f_tone; } if (request.emojify) { flags |= Flag::f_emojify; @@ -53,9 +53,14 @@ mtpRequestId ComposeWithAi::request( request.translateToLang.isEmpty() ? MTPstring() : MTP_string(request.translateToLang), - request.changeTone.isEmpty() - ? MTPstring() - : MTP_string(request.changeTone) + request.tone + ? (request.tone->id + ? MTP_inputAiComposeToneID( + MTP_long(request.tone->id), + MTP_long(request.tone->accessHash)) + : MTP_inputAiComposeToneDefault( + MTP_string(request.tone->defaultTone))) + : MTPInputAiComposeTone() )).done([=, done = std::move(done)]( const MTPmessages_ComposedMessageWithAI &result) mutable { const auto &data = result.data(); diff --git a/Telegram/SourceFiles/api/api_compose_with_ai.h b/Telegram/SourceFiles/api/api_compose_with_ai.h index 9a87aa0542..6690ce3d8c 100644 --- a/Telegram/SourceFiles/api/api_compose_with_ai.h +++ b/Telegram/SourceFiles/api/api_compose_with_ai.h @@ -23,12 +23,25 @@ namespace Api { class ComposeWithAi final { public: + struct ToneRef { + QString defaultTone; + uint64 id = 0; + uint64 accessHash = 0; + }; + struct Request { TextWithEntities text; QString translateToLang; - QString changeTone; + std::optional tone; bool proofread = false; bool emojify = false; + + void setDefaultTone(const QString &type) { + tone = ToneRef{ .defaultTone = type }; + } + void setCustomTone(uint64 id, uint64 accessHash) { + tone = ToneRef{ .id = id, .accessHash = accessHash }; + } }; struct DiffEntity { diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index f292b623aa..95837ee07d 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/top_peers.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" +#include "data/data_ai_compose_tones.h" #include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" @@ -2779,6 +2780,10 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().api().ringtones().applyUpdate(); } break; + case mtpc_updateAiComposeTones: { + session().data().aiComposeTones().applyUpdate(); + } break; + case mtpc_updateTranscribedAudio: { const auto &data = update.c_updateTranscribedAudio(); _session->api().transcribes().apply(data); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 5a1c8cbbd4..79f00ea1d0 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1246,6 +1246,9 @@ aiComposeBadge: RoundButton(customEmojiTextBadge) { } aiComposeBadgeMargin: margins(0px, 2px, 0px, 0px); +aiComposeAddStyleIcon: icon {{ "menu/edit_stars_add", aiComposeButtonFg }}; +aiComposeAddStyleIconOver: icon {{ "menu/edit_stars_add", aiComposeButtonFgActive }}; + aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; aiComposeCardPadding: margins(12px, 16px, 16px, 16px); diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 0d51c9d322..44c9ea1cbb 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_compose_with_ai.h" #include "apiwrap.h" +#include "boxes/create_ai_tone_box.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_lottie.h" @@ -16,10 +17,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "core/core_settings.h" #include "core/ui_integration.h" +#include "data/data_ai_compose_tones.h" #include "data/data_document.h" +#include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" -#include "data/data_session.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" #include "main/main_app_config.h" @@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/platform/platform_language.h" #include "ui/boxes/about_cocoon_box.h" #include "ui/boxes/choose_language_box.h" +#include "ui/boxes/confirm_box.h" #include "ui/chat/chat_style.h" #include "ui/controls/labeled_emoji_tabs.h" #include "ui/controls/send_button.h" @@ -46,16 +49,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/tooltip.h" #include "styles/style_basic.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_widgets.h" #include #include +#include + namespace HistoryView::Controls { namespace { @@ -232,6 +239,27 @@ enum class CardState { return result; } +[[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor( + const Data::AiComposeTone &tone) { + return { + .id = tone.isDefault ? tone.defaultType : QString::number(tone.id), + .label = tone.title, + .customEmojiData = tone.emojiId + ? Data::SerializeCustomEmojiId(tone.emojiId) + : QString(), + }; +} + +[[nodiscard]] std::vector ResolveStyleDescriptors( + const std::vector &tones) { + auto result = std::vector(); + result.reserve(tones.size()); + for (const auto &tone : tones) { + result.push_back(ResolveStyleDescriptor(tone)); + } + return result; +} + [[nodiscard]] std::vector ResolveTranslateStyleDescriptors( not_null session, const std::vector &styles) { @@ -249,6 +277,17 @@ enum class CardState { return result; } +[[nodiscard]] auto WithAddStyleTab(std::vector tabs) +-> std::vector { + tabs.push_back({ + .id = u"_add_style"_q, + .label = tr::lng_ai_compose_add_style(tr::now), + .icon = &st::aiComposeAddStyleIcon, + .iconActive = &st::aiComposeAddStyleIconOver, + }); + return tabs; +} + [[nodiscard]] TextWithEntities LoadingTitleSparkle( not_null session) { const auto sparkles = ChatHelpers::GenerateLocalTgsSticker( @@ -364,6 +403,7 @@ public: [[nodiscard]] bool hasResult() const; [[nodiscard]] const TextWithEntities &result() const; [[nodiscard]] const std::vector &stylesData() const; + [[nodiscard]] const std::vector &tones() const; void setReadyChangedCallback(Fn callback); void setLoadingChangedCallback(Fn callback); void setPremiumFloodCallback(Fn callback); @@ -400,6 +440,7 @@ private: const TextWithEntities _original; const LanguageId _detectedFrom; LanguageId _to; + const std::vector _tones; const std::vector _stylesData; const std::vector _translateStylesData; QPointer _tabs; @@ -934,8 +975,8 @@ ComposeAiContent::ComposeAiContent( , _original(std::move(args.text)) , _detectedFrom(Platform::Language::Recognize(_original.text)) , _to(DefaultAiTranslateTo(_detectedFrom)) -, _stylesData(ResolveStyleDescriptors( - _session->appConfig().aiComposeStyles())) +, _tones(_session->data().aiComposeTones().list()) +, _stylesData(ResolveStyleDescriptors(_tones)) , _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData)) , _preview( Ui::CreateChild( @@ -971,6 +1012,10 @@ const std::vector &ComposeAiContent::stylesData() const { return _stylesData; } +const std::vector &ComposeAiContent::tones() const { + return _tones; +} + void ComposeAiContent::setReadyChangedCallback(Fn callback) { _readyChanged = std::move(callback); } @@ -994,7 +1039,7 @@ void ComposeAiContent::setStyleTabs( _stylesWrap->setDuration(0); _styles = stylesWrap->entity(); _styles->setChangedCallback([=](int index) { - if (index >= 0 && index < int(_stylesData.size())) { + if (index >= 0 && index < int(_tones.size())) { const auto wasNoSelection = (_styleIndex < 0); _styleIndex = index; updateTitles(); @@ -1004,6 +1049,9 @@ void ComposeAiContent::setStyleTabs( _styleSelected(); } } + } else if (index == int(_tones.size())) { + _styles->setActive(_styleIndex); + _box->uiShow()->show(Box(CreateAiToneBox, _session, nullptr)); } }); _styles->setActive(_styleIndex); @@ -1173,13 +1221,21 @@ void ComposeAiContent::request() { .emojify = (_mode != ComposeAiMode::Fix) && _emojify, }; switch (_mode) { - case ComposeAiMode::Translate: + case ComposeAiMode::Translate: { request.translateToLang = _to.twoLetterCode(); - request.changeTone = currentTranslateStyle(); - break; + const auto style = currentTranslateStyle(); + if (!style.isEmpty()) { + request.setDefaultTone(style); + } + } break; case ComposeAiMode::Style: - if (_styleIndex >= 0) { - request.changeTone = _stylesData[_styleIndex].id; + if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) { + const auto &tone = _tones[_styleIndex]; + if (tone.isDefault) { + request.setDefaultTone(tone.defaultType); + } else { + request.setCustomTone(tone.id, tone.accessHash); + } } break; case ComposeAiMode::Fix: @@ -1440,11 +1496,63 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { pinnedToTop, object_ptr( pinnedToTop, - content->stylesData(), + WithAddStyleTab(content->stylesData()), std::move(emojiFactory)), tabsSkip), st::aiComposeContentMargin); stylesWrap->hide(anim::type::instant); + + const auto contextMenu = box->lifetime().make_state< + base::unique_qptr>(); + stylesWrap->entity()->setContextMenuCallback([=](int index, QPoint globalPos) { + const auto &tones = content->tones(); + if (index < 0 || index >= int(tones.size())) { + return; + } + const auto &tone = tones[index]; + if (!tone.creator) { + return; + } + *contextMenu = base::make_unique_q( + stylesWrap->entity(), + st::popupMenuWithIcons); + const auto toneCopy = tone; + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_edit(tr::now), + [=] { + box->uiShow()->show(Box( + EditAiToneBox, + session, + toneCopy, + nullptr)); + }, + &st::menuIconEdit); + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_share(tr::now), + [=] { + QGuiApplication::clipboard()->setText( + session->createInternalLinkFull( + "aistyle/" + toneCopy.slug)); + box->showToast( + tr::lng_ai_compose_tone_link_copied(tr::now)); + }, + &st::menuIconShare); + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_delete(tr::now), + [=] { + box->uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_delete_sure(), + .confirmed = [=](Fn &&close) { + close(); + session->data().aiComposeTones().remove(toneCopy); + }, + .confirmText = tr::lng_ai_compose_tone_delete(), + })); + }, + &st::menuIconDelete); + (*contextMenu)->popup(globalPos); + }); + content->setModeTabs(tabs); content->setStyleTabs(stylesWrap); diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp new file mode 100644 index 0000000000..55c0462da1 --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -0,0 +1,129 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/create_ai_tone_box.h" + +#include "data/data_ai_compose_tones.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/layers/generic_box.h" +#include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" + +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +namespace { + +void SetupToneBox( + not_null box, + not_null session, + const QString &initialName, + const QString &initialPrompt, + rpl::producer title, + Fn submit) { + box->setTitle(std::move(title)); + + const auto name = box->addRow(object_ptr( + box, + st::defaultInputField, + tr::lng_ai_compose_tone_name(), + initialName)); + + Ui::AddSkip(box->verticalLayout()); + + const auto promptSt = box->lifetime().make_state( + st::newGroupDescription); + const auto prompt = box->addRow(object_ptr( + box, + *promptSt, + Ui::InputField::Mode::MultiLine, + tr::lng_ai_compose_tone_prompt(), + initialPrompt)); + + box->setFocusCallback([=] { + name->setFocusFast(); + }); + + const auto save = [=] { + const auto nameText = name->getLastText().trimmed(); + const auto promptText = prompt->getLastText().trimmed(); + if (nameText.isEmpty() || promptText.isEmpty()) { + if (nameText.isEmpty()) { + name->showError(); + } + if (promptText.isEmpty()) { + prompt->showError(); + } + return; + } + submit(nameText, promptText); + }; + + box->addButton(tr::lng_ai_compose_tone_save(), save); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +} // namespace + +void CreateAiToneBox( + not_null box, + not_null session, + Fn saved) { + SetupToneBox( + box, + session, + QString(), + QString(), + tr::lng_ai_compose_create_tone_title(), + [=](const QString &name, const QString &prompt) { + session->data().aiComposeTones().create( + name, + prompt, + 0, + false, + [=](Data::AiComposeTone) { + box->closeBox(); + if (saved) { + saved(); + } + }); + }); +} + +void EditAiToneBox( + not_null box, + not_null session, + const Data::AiComposeTone &tone, + Fn saved) { + const auto toneId = tone.id; + const auto toneAccessHash = tone.accessHash; + SetupToneBox( + box, + session, + tone.title, + tone.prompt, + tr::lng_ai_compose_edit_tone_title(), + [=](const QString &name, const QString &prompt) { + auto toneCopy = Data::AiComposeTone(); + toneCopy.id = toneId; + toneCopy.accessHash = toneAccessHash; + session->data().aiComposeTones().update( + toneCopy, + name, + prompt, + std::nullopt, + std::nullopt, + [=](Data::AiComposeTone) { + box->closeBox(); + if (saved) { + saved(); + } + }); + }); +} diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h new file mode 100644 index 0000000000..cfcbf1647e --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -0,0 +1,31 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +struct AiComposeTone; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class GenericBox; +} // namespace Ui + +void CreateAiToneBox( + not_null box, + not_null session, + Fn saved = nullptr); + +void EditAiToneBox( + not_null box, + not_null session, + const Data::AiComposeTone &tone, + Fn saved = nullptr); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 1ae2a02ee3..62c3e924fc 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "data/components/credits.h" +#include "data/data_ai_compose_tones.h" #include "data/data_birthday.h" #include "data/data_channel.h" #include "data/data_document.h" @@ -294,6 +295,48 @@ bool ShowTheme( return true; } +bool ShowAiStyle( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto slug = match->captured(1); + Core::App().hideMediaView(); + const auto weak = base::make_weak(controller); + auto &tones = controller->session().data().aiComposeTones(); + tones.resolve(slug, [=](Data::AiComposeTone tone) { + const auto strong = weak.get(); + if (!strong) { + return; + } + strong->window().show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_save_sure( + tr::now, + lt_title, + tone.title), + .confirmed = [=](Fn &&close) { + close(); + strong->session().data().aiComposeTones().save( + tone, + false); + strong->window().showToast( + tr::lng_ai_compose_tone_saved(tr::now)); + }, + .confirmText = tr::lng_ai_compose_tone_save(), + })); + }, [=](const MTP::Error &error) { + const auto strong = weak.get(); + if (!strong) { + return; + } + strong->window().showToast(tr::lng_ai_compose_error(tr::now)); + }); + controller->window().activate(); + return true; +} + void ShowLanguagesBox(Window::SessionController *controller) { static auto Guard = base::binary_guard(); Guard = LanguageBox::Show(controller); @@ -1661,6 +1704,10 @@ const std::vector &LocalUrlHandlers() { u"^addtheme/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, ShowTheme }, + { + u"^aistyle/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, + ShowAiStyle + }, { u"^setlanguage/?(\\?lang=([a-zA-Z0-9\\.\\_\\-]+))?(&|$)"_q, SetLanguage @@ -1878,6 +1925,8 @@ QString TryConvertUrlToLocal(QString url) { return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2)); } else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://addtheme?slug="_q + url_encode(themeMatch->captured(1)); + } else if (const auto aiStyleMatch = regex_match(u"^aistyle/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { + return u"tg://aistyle?slug="_q + url_encode(aiStyleMatch->captured(1)); } else if (const auto languageMatch = regex_match(u"^setlanguage/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://setlanguage?lang="_q + url_encode(languageMatch->captured(1)); } else if (const auto shareUrlMatch = regex_match(u"^share/url/?\\?(.+)$"_q, query, matchOptions)) { diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp new file mode 100644 index 0000000000..1c2b253d5e --- /dev/null +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -0,0 +1,225 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_ai_compose_tones.h" + +#include "main/main_session.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kRefreshInterval = 3600 * crl::time(1000); + +} // namespace + +AiComposeTones::AiComposeTones(not_null session) +: _session(session) +, _refreshTimer([=] { refresh(); }) { + refresh(); + _refreshTimer.callEach(kRefreshInterval); +} + +void AiComposeTones::refresh() { + if (_refreshRequestId) { + return; + } + _refreshRequestId = _session->api().request(MTPaicompose_GetTones( + MTP_long(_hash) + )).done([=](const MTPaicompose_Tones &result) { + _refreshRequestId = 0; + result.match([&](const MTPDaicompose_tones &data) { + _hash = data.vhash().v; + parseTones(data.vtones().v); + _updates.fire({}); + }, [](const MTPDaicompose_tonesNotModified &) { + }); + }).fail([=] { + _refreshRequestId = 0; + }).send(); +} + +void AiComposeTones::parseTones(const QVector &list) { + _list.clear(); + _list.reserve(list.size()); + for (const auto &tone : list) { + _list.push_back(parseTone(tone)); + } +} + +AiComposeTone AiComposeTones::parseTone( + const MTPAiComposeTone &tone) const { + return tone.match([&](const MTPDaiComposeTone &data) { + return AiComposeTone{ + .id = data.vid().v, + .accessHash = data.vaccess_hash().v, + .slug = qs(data.vslug()), + .title = qs(data.vtitle()), + .emojiId = data.vemoji_id().value_or_empty(), + .prompt = qs(data.vprompt()), + .installsCount = data.vinstalls_count().value_or_empty(), + .authorId = data.vauthor_id() + ? UserId(data.vauthor_id()->v) + : UserId(0), + .creator = data.is_creator(), + }; + }, [&](const MTPDaiComposeToneDefault &data) { + return AiComposeTone{ + .title = qs(data.vtitle()), + .emojiId = data.vemoji_id().v, + .isDefault = true, + .defaultType = qs(data.vtone()), + }; + }); +} + +void AiComposeTones::create( + const QString &title, + const QString &prompt, + DocumentId emojiId, + bool displayAuthor, + Fn done, + Fn fail) { + using Flag = MTPaicompose_CreateTone::Flag; + auto flags = MTPaicompose_CreateTone::Flags(0); + if (displayAuthor) { + flags |= Flag::f_display_author; + } + if (emojiId) { + flags |= Flag::f_emoji_id; + } + _session->api().request(MTPaicompose_CreateTone( + MTP_flags(flags), + MTP_long(emojiId), + MTP_string(title), + MTP_string(prompt) + )).done([=](const MTPAiComposeTone &result) { + auto parsed = parseTone(result); + if (done) { + done(parsed); + } + refresh(); + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } + }).send(); +} + +void AiComposeTones::update( + const AiComposeTone &tone, + std::optional title, + std::optional prompt, + std::optional emojiId, + std::optional displayAuthor, + Fn done, + Fn fail) { + using Flag = MTPaicompose_UpdateTone::Flag; + auto flags = MTPaicompose_UpdateTone::Flags(0); + if (displayAuthor) { + flags |= Flag::f_display_author; + } + if (emojiId) { + flags |= Flag::f_emoji_id; + } + if (title) { + flags |= Flag::f_title; + } + if (prompt) { + flags |= Flag::f_prompt; + } + _session->api().request(MTPaicompose_UpdateTone( + MTP_flags(flags), + toneToMTP(tone), + displayAuthor + ? (*displayAuthor ? MTP_boolTrue() : MTP_boolFalse()) + : MTPBool(), + MTP_long(emojiId.value_or(0)), + MTP_string(title.value_or(QString())), + MTP_string(prompt.value_or(QString())) + )).done([=](const MTPAiComposeTone &result) { + auto parsed = parseTone(result); + if (done) { + done(parsed); + } + refresh(); + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } + }).send(); +} + +void AiComposeTones::save( + const AiComposeTone &tone, + bool unsave, + Fn done) { + _session->api().request(MTPaicompose_SaveTone( + toneToMTP(tone), + unsave ? MTP_boolTrue() : MTP_boolFalse() + )).done([=] { + if (done) { + done(); + } + refresh(); + }).fail([=] { + }).send(); +} + +void AiComposeTones::remove( + const AiComposeTone &tone, + Fn done) { + _session->api().request(MTPaicompose_DeleteTone( + toneToMTP(tone) + )).done([=] { + if (done) { + done(); + } + refresh(); + }).fail([=] { + }).send(); +} + +void AiComposeTones::resolve( + const QString &slug, + Fn done, + Fn fail) { + _session->api().request(MTPaicompose_GetTone( + MTP_inputAiComposeToneSlug(MTP_string(slug)) + )).done([=](const MTPAiComposeTone &result) { + if (done) { + done(parseTone(result)); + } + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } + }).send(); +} + +void AiComposeTones::applyUpdate() { + refresh(); +} + +MTPInputAiComposeTone AiComposeTones::toneToMTP( + const AiComposeTone &tone) const { + return tone.isDefault + ? MTP_inputAiComposeToneDefault(MTP_string(tone.defaultType)) + : MTP_inputAiComposeToneID( + MTP_long(tone.id), + MTP_long(tone.accessHash)); +} + +const std::vector &AiComposeTones::list() const { + return _list; +} + +rpl::producer<> AiComposeTones::updated() const { + return _updates.events(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.h b/Telegram/SourceFiles/data/data_ai_compose_tones.h new file mode 100644 index 0000000000..c17e8f2738 --- /dev/null +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.h @@ -0,0 +1,90 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/timer.h" + +namespace Main { +class Session; +} // namespace Main + +namespace MTP { +class Error; +} // namespace MTP + +namespace Data { + +struct AiComposeTone { + uint64 id = 0; + uint64 accessHash = 0; + QString slug; + QString title; + DocumentId emojiId = 0; + QString prompt; + int installsCount = 0; + UserId authorId = 0; + bool creator = false; + bool isDefault = false; + QString defaultType; +}; + +class AiComposeTones final { +public: + explicit AiComposeTones(not_null session); + + void refresh(); + [[nodiscard]] const std::vector &list() const; + [[nodiscard]] rpl::producer<> updated() const; + + void create( + const QString &title, + const QString &prompt, + DocumentId emojiId, + bool displayAuthor, + Fn done, + Fn fail = nullptr); + void update( + const AiComposeTone &tone, + std::optional title, + std::optional prompt, + std::optional emojiId, + std::optional displayAuthor, + Fn done, + Fn fail = nullptr); + void save( + const AiComposeTone &tone, + bool unsave, + Fn done = nullptr); + void remove( + const AiComposeTone &tone, + Fn done = nullptr); + void resolve( + const QString &slug, + Fn done, + Fn fail = nullptr); + + void applyUpdate(); + + [[nodiscard]] MTPInputAiComposeTone toneToMTP( + const AiComposeTone &tone) const; + +private: + void parseTones(const QVector &list); + [[nodiscard]] AiComposeTone parseTone( + const MTPAiComposeTone &tone) const; + + const not_null _session; + uint64 _hash = 0; + mtpRequestId _refreshRequestId = 0; + std::vector _list; + rpl::event_stream<> _updates; + base::Timer _refreshTimer; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 10d1340d6d..b23bd4234d 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -192,7 +192,7 @@ bool PollData::applyResults(const MTPPollResults &results) { auto changed = (newTotalVoters != totalVoters); const auto setCanViewStats = [&](bool value) { const auto previous = (_flags & Flag::CanViewStats); - if (previous == value) { + if (bool(previous) == value) { return; } if (value) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index c5313fb154..96eadb153d 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/sponsored_messages.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" +#include "data/data_ai_compose_tones.h" #include "data/data_bot_app.h" #include "data/data_changes.h" #include "data/data_group_call.h" @@ -242,6 +243,7 @@ Session::Session(not_null session) , _pollsClosingTimer([=] { checkPollsClosings(); }) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) , _groups(this) +, _aiComposeTones(std::make_unique(session)) , _chatsFilters(std::make_unique(this)) , _cloudThemes(std::make_unique(session)) , _sendActionManager(std::make_unique()) diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index e66c1b81d3..f41afac491 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -59,6 +59,7 @@ class SendActionManager; class Reactions; class EmojiStatuses; class ForumIcons; +class AiComposeTones; class ChatFilters; class CloudThemes; class Streaming; @@ -174,6 +175,9 @@ public: [[nodiscard]] const Groups &groups() const { return _groups; } + [[nodiscard]] AiComposeTones &aiComposeTones() const { + return *_aiComposeTones; + } [[nodiscard]] ChatFilters &chatsFilters() const { return *_chatsFilters; } @@ -1325,6 +1329,7 @@ private: mutable base::flat_map> _messagesWithPeer; Groups _groups; + const std::unique_ptr _aiComposeTones; const std::unique_ptr _chatsFilters; const std::unique_ptr _cloudThemes; const std::unique_ptr _sendActionManager; diff --git a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp index b4b7b28189..7c670a311d 100644 --- a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp +++ b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/compose_ai_box.h" #include "config.h" #include "core/mime_type.h" +#include "data/data_ai_compose_tones.h" +#include "data/data_session.h" #include "history/view/controls/history_view_compose_ai_button.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" @@ -36,7 +38,8 @@ bool HasEnoughLinesForAi( not_null session, not_null field) { if (HideAiButtonOption.value() - || session->appConfig().aiComposeStyles().empty()) { + || (session->appConfig().aiComposeStyles().empty() + && session->data().aiComposeTones().list().empty())) { return false; } const auto &style = field->st().style; diff --git a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp index d60e5a77b6..ef086baa37 100644 --- a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp +++ b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include #include #include @@ -58,15 +59,18 @@ public: void setSelected(bool selected); void setExtraPadding(int extra); + void setContextMenuCallback(Fn callback); [[nodiscard]] const QString &id() const; protected: void paintEvent(QPaintEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; [[nodiscard]] QImage prepareRippleMask() const override; private: const LabeledEmojiTab _descriptor; std::unique_ptr _custom; + Fn _contextMenuCallback; bool _selected = false; int _extraPadding = 0; @@ -110,7 +114,7 @@ LabeledEmojiTabs::Button::Button( const auto padding = st::aiComposeStyleButtonPadding; const auto labelWidth = st::aiComposeStyleLabelFont->width( _descriptor.label); - const auto emojiWidth = (_custom || _descriptor.emoji) + const auto emojiWidth = (_custom || _descriptor.emoji || _descriptor.icon) ? (Emoji::GetSizeLarge() / style::DevicePixelRatio()) : 0; return padding.left() @@ -180,6 +184,13 @@ void LabeledEmojiTabs::Button::paintEvent(QPaintEvent *e) { Emoji::GetSizeLarge(), left, st::aiComposeStyleEmojiTop); + } else if (_descriptor.icon) { + const auto &icon = _selected + ? *_descriptor.iconActive + : *_descriptor.icon; + icon.paintInCenter( + p, + QRect(0, 0, width(), st::aiComposeStyleLabelTop)); } p.setPen(_selected @@ -205,6 +216,17 @@ QImage LabeledEmojiTabs::Button::prepareRippleMask() const { }); } +void LabeledEmojiTabs::Button::setContextMenuCallback( + Fn callback) { + _contextMenuCallback = std::move(callback); +} + +void LabeledEmojiTabs::Button::contextMenuEvent(QContextMenuEvent *e) { + if (_contextMenuCallback) { + _contextMenuCallback(e->globalPos()); + } +} + LabeledEmojiScrollTabs::DragScroll::DragScroll( not_null parent, not_null scroll, @@ -332,6 +354,18 @@ void LabeledEmojiTabs::setChangedCallback(Fn callback) { _changed = std::move(callback); } +void LabeledEmojiTabs::setContextMenuCallback( + Fn callback) { + _contextMenu = std::move(callback); + for (auto i = 0; i != int(_buttons.size()); ++i) { + _buttons[i]->setContextMenuCallback([=](QPoint globalPos) { + if (_contextMenu) { + _contextMenu(i, globalPos); + } + }); + } +} + void LabeledEmojiTabs::setActive(int index) { if (index < -1 || index >= int(_buttons.size())) { return; @@ -530,6 +564,11 @@ void LabeledEmojiScrollTabs::setChangedCallback(Fn callback) { _inner->setChangedCallback(std::move(callback)); } +void LabeledEmojiScrollTabs::setContextMenuCallback( + Fn callback) { + _inner->setContextMenuCallback(std::move(callback)); +} + void LabeledEmojiScrollTabs::setActive(int index) { _inner->setActive(index); } diff --git a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h index 63dfb9c7ef..08300f5d08 100644 --- a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h +++ b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h @@ -25,6 +25,8 @@ struct LabeledEmojiTab { QString label; EmojiPtr emoji = nullptr; QString customEmojiData; + const style::icon *icon = nullptr; + const style::icon *iconActive = nullptr; }; class LabeledEmojiScrollTabs; @@ -39,6 +41,7 @@ public: Text::CustomEmojiFactory factory); void setChangedCallback(Fn callback); + void setContextMenuCallback(Fn callback); void setActive(int index); void resizeForOuterWidth(int outerWidth); [[nodiscard]] QString currentId() const; @@ -53,6 +56,7 @@ private: std::vector> _buttons; Fn _changed; + Fn _contextMenu; int _active = -1; rpl::event_stream _requestShown; @@ -67,6 +71,7 @@ public: ~LabeledEmojiScrollTabs(); void setChangedCallback(Fn callback); + void setContextMenuCallback(Fn callback); void setActive(int index); void setPaintOuterCorners(bool paint); void scrollToActive(); From 6df177c8dd5dccc9b0eabf582d3be4c3c84b7a43 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 14 Apr 2026 11:23:10 +0700 Subject: [PATCH 090/154] Remove legacy app-config ai tones. --- Telegram/SourceFiles/boxes/compose_ai_box.cpp | 20 ----------- Telegram/SourceFiles/main/main_app_config.cpp | 34 ------------------- Telegram/SourceFiles/main/main_app_config.h | 8 ----- .../ui/controls/compose_ai_button_factory.cpp | 4 +-- 4 files changed, 1 insertion(+), 65 deletions(-) diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 44c9ea1cbb..65fa2d3e89 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -24,7 +24,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "settings/sections/settings_premium.h" #include "spellcheck/platform/platform_language.h" @@ -220,25 +219,6 @@ enum class CardState { opacity); } -[[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor( - const Main::AppConfig::AiComposeStyle &style) { - return { - .id = style.type, - .label = style.title, - .customEmojiData = Data::SerializeCustomEmojiId(style.emojiId), - }; -} - -[[nodiscard]] std::vector ResolveStyleDescriptors( - const std::vector &styles) { - auto result = std::vector(); - result.reserve(styles.size()); - for (const auto &style : styles) { - result.push_back(ResolveStyleDescriptor(style)); - } - return result; -} - [[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor( const Data::AiComposeTone &tone) { return { diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 8008b10f9b..e6b39e2983 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -341,7 +341,6 @@ void AppConfig::refresh(bool force) { } updateIgnoredRestrictionReasons(std::move(was)); - _aiComposeStyles.reset(); _groupCallColorings = {}; DEBUG_LOG(("getAppConfig result handled.")); @@ -552,39 +551,6 @@ bool AppConfig::newRequirePremiumFree() const { false); } -std::vector AppConfig::aiComposeStyles() const { - if (_aiComposeStyles) { - return *_aiComposeStyles; - } - _aiComposeStyles = getValue(u"ai_compose_styles"_q, [&](const auto &value) { - return value.match([&](const MTPDjsonArray &data) { - auto result = std::vector(); - result.reserve(data.vvalue().v.size()); - for (const auto &entry : data.vvalue().v) { - if (entry.type() != mtpc_jsonArray) { - return std::vector(); - } - const auto &list = entry.c_jsonArray().vvalue().v; - if (list.size() < 3 - || (list[0].type() != mtpc_jsonString) - || (list[1].type() != mtpc_jsonString) - || (list[2].type() != mtpc_jsonString)) { - return std::vector(); - } - result.push_back({ - .type = qs(list[0].c_jsonString().vvalue()), - .emojiId = qs(list[1].c_jsonString().vvalue()).toULongLong(), - .title = qs(list[2].c_jsonString().vvalue()), - }); - } - return result; - }, [&](const auto &) { - return std::vector(); - }); - }); - return *_aiComposeStyles; -} - auto AppConfig::groupCallColorings() const -> std::vector { if (!_groupCallColorings.empty()) { return _groupCallColorings; diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index b7adb2e291..decc2aa5ff 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -138,13 +138,6 @@ public: [[nodiscard]] int64 stakeDiceNanoTonMax() const; [[nodiscard]] std::vector stakeDiceNanoTonSuggested() const; - struct AiComposeStyle { - QString type; - DocumentId emojiId = 0; - QString title; - }; - [[nodiscard]] std::vector aiComposeStyles() const; - using StarsColoring = Calls::Group::Ui::StarsColoring; [[nodiscard]] std::vector groupCallColorings() const; @@ -200,7 +193,6 @@ private: std::vector _startRefPrefixes; - mutable std::optional> _aiComposeStyles; mutable std::vector _groupCallColorings; crl::time _lastFrozenRefresh = 0; diff --git a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp index 7c670a311d..ba6456db8e 100644 --- a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp +++ b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp @@ -15,7 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/view/controls/history_view_compose_ai_button.h" #include "lang/lang_keys.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "ui/chat/attach/attach_prepare.h" #include "ui/text/text.h" @@ -38,8 +37,7 @@ bool HasEnoughLinesForAi( not_null session, not_null field) { if (HideAiButtonOption.value() - || (session->appConfig().aiComposeStyles().empty() - && session->data().aiComposeTones().list().empty())) { + || session->data().aiComposeTones().list().empty()) { return false; } const auto &style = field->st().style; From dc0c7ec806609e0b2e1ba93c5eaee2d7f6ae9a27 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 16 Apr 2026 10:28:59 +0700 Subject: [PATCH 091/154] Update API scheme on layer 225. Edit tone icon. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/boxes/boxes.style | 17 ++ Telegram/SourceFiles/boxes/compose_ai_box.cpp | 286 +++++++++++++----- .../SourceFiles/boxes/create_ai_tone_box.cpp | 283 ++++++++++++++++- .../data/data_ai_compose_tones.cpp | 53 +++- Telegram/SourceFiles/mtproto/scheme/api.tl | 9 +- 6 files changed, 558 insertions(+), 92 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9971741fdb..f4c17030bd 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7922,6 +7922,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_add_style" = "Add Style"; "lng_ai_compose_create_tone_title" = "New Style"; "lng_ai_compose_edit_tone_title" = "Edit Style"; +"lng_ai_compose_tone_icon_title" = "Style Icon"; "lng_ai_compose_tone_name" = "Name"; "lng_ai_compose_tone_prompt" = "Prompt"; "lng_ai_compose_tone_save" = "Save"; @@ -7932,6 +7933,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_save_sure" = "Do you want to save the style {title}?"; "lng_ai_compose_tone_saved" = "Style saved."; "lng_ai_compose_tone_link_copied" = "Style link copied."; +"lng_ai_compose_author" = "This style was created by {user}"; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 79f00ea1d0..a12102956e 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1249,6 +1249,12 @@ aiComposeBadgeMargin: margins(0px, 2px, 0px, 0px); aiComposeAddStyleIcon: icon {{ "menu/edit_stars_add", aiComposeButtonFg }}; aiComposeAddStyleIconOver: icon {{ "menu/edit_stars_add", aiComposeButtonFgActive }}; +aiToneIconPreviewSize: 80px; +aiToneIconPreviewBottomSkip: 12px; +aiToneIconPreviewTopSkip: 4px; +aiToneIconPreviewBg: windowBgOver; +aiToneIconPreviewInnerSize: 54px; + aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; aiComposeCardPadding: margins(12px, 16px, 16px, 16px); @@ -1283,6 +1289,17 @@ aiComposeCopyButton: IconButton(aiComposeExpandButton) { iconOver: aiComposeCopyIcon; } +aiComposeAuthorLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 0px; + maxHeight: 20px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineActive; + } +} +aiComposeAuthorLabelTop: 4px; + aiComposeBoxButton: RoundButton(defaultActiveButton) { height: 42px; textTop: 12px; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 65fa2d3e89..076f5a52c0 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -337,6 +337,7 @@ public: void setEmojifyChecked(bool checked); void setState(CardState state); void setResultText(TextWithEntities text); + void setAuthorId(UserId authorId); void setShow(std::shared_ptr show); protected: @@ -348,6 +349,7 @@ private: void updateOriginalToggleIcon(); const Ui::Text::MarkedContext _context; + const not_null _session; const TextWithEntities _original; const not_null _originalTitle; const not_null _originalBody; @@ -355,6 +357,7 @@ private: const not_null _resultTitle; const not_null _resultBody; const not_null _copy; + const not_null _authorLabel; const not_null _emojify; Fn _resized; Fn _chooseCallback; @@ -367,6 +370,7 @@ private: bool _dividerVisible = false; int _dividerTop = 0; CardState _state = CardState::Waiting; + UserId _authorId = UserId(0); Ui::SkeletonAnimation _skeleton; std::array _diffColors; @@ -393,6 +397,7 @@ public: [[nodiscard]] bool hasStyleSelection() const; void setModeTabs(not_null tabs); void setStyleTabs(not_null*> stylesWrap); + void refreshTones(); void start(); protected: @@ -420,9 +425,9 @@ private: const TextWithEntities _original; const LanguageId _detectedFrom; LanguageId _to; - const std::vector _tones; - const std::vector _stylesData; - const std::vector _translateStylesData; + std::vector _tones; + std::vector _stylesData; + std::vector _translateStylesData; QPointer _tabs; QPointer _styles; QPointer> _stylesWrap; @@ -599,6 +604,7 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( std::shared_ptr chatStyle) : RpWidget(parent) , _context(Core::TextContext({ .session = session })) +, _session(session) , _original(std::move(original)) , _originalTitle(Ui::CreateChild( this, @@ -618,6 +624,9 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( , _copy(Ui::CreateChild( this, st::aiComposeCopyButton)) +, _authorLabel(Ui::CreateChild( + this, + st::aiComposeAuthorLabel)) , _emojify( Ui::CreateChild( this, @@ -644,6 +653,7 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( }; watchHeight(_originalBody); watchHeight(_resultBody); + watchHeight(_authorLabel); _diffColors[0] = { &st::boxTextFgGood->p, &st::boxTextFgGood->p }; _diffColors[1] = { &st::attentionButtonFg->p, &st::attentionButtonFg->p }; _resultBody->setColors(_diffColors); @@ -669,6 +679,7 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( setResultTitle(tr::lng_ai_compose_result(tr::now, tr::marked)); _resultBody->setMarkedText(_original, _context); _copy->setVisible(false); + _authorLabel->setVisible(false); updateOriginalToggleIcon(); if (chatStyle) { const auto style = chatStyle; @@ -747,6 +758,7 @@ void ComposeAiPreviewCard::setState(CardState state) { case CardState::Failed: _resultBody->setMarkedText(_original, _context); _copy->setVisible(false); + _authorLabel->setVisible(false); if (wasLoading) { _skeleton.stop(); } @@ -754,10 +766,12 @@ void ComposeAiPreviewCard::setState(CardState state) { case CardState::Loading: _resultBody->setMarkedText(_original, _context); _copy->setVisible(false); + _authorLabel->setVisible(false); _skeleton.start(); break; case CardState::Ready: _copy->setVisible(true); + _authorLabel->setVisible(_authorId != 0); if (wasLoading) { _skeleton.stop(); } @@ -771,6 +785,37 @@ void ComposeAiPreviewCard::setResultText(TextWithEntities text) { refreshGeometry(); } +void ComposeAiPreviewCard::setAuthorId(UserId authorId) { + if (_authorId == authorId) { + return; + } + _authorId = authorId; + if (const auto user = _session->data().userLoaded(authorId)) { + const auto name = user->shortName(); + auto mention = tr::marked(name); + mention.entities.push_back(EntityInText( + EntityType::MentionName, + 0, + name.size(), + TextUtilities::MentionNameDataFromFields({ + .selfId = _session->userId().bare, + .userId = authorId.bare, + .accessHash = user->accessHash(), + }))); + _authorLabel->setMarkedText( + tr::lng_ai_compose_author( + tr::now, + lt_user, + std::move(mention), + tr::marked), + _context); + } else { + _authorLabel->setMarkedText({}); + _authorId = UserId(0); + } + refreshGeometry(); +} + void ComposeAiPreviewCard::setShow(std::shared_ptr show) { const auto setupFilter = [&](not_null label) { label->setClickHandlerFilter([=]( @@ -790,6 +835,7 @@ void ComposeAiPreviewCard::setShow(std::shared_ptr show) { }; setupFilter(_originalBody); setupFilter(_resultBody); + setupFilter(_authorLabel); } int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { @@ -883,7 +929,8 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { const auto lineHeight = _resultBody->st().style.lineHeight ? _resultBody->st().style.lineHeight : _resultBody->st().style.font->height; - if (!_copy->isHidden()) { + const auto authorVisible = !_authorLabel->isHidden(); + if (!_copy->isHidden() && !authorVisible) { _resultBody->setSkipBlock( _copy->width(), lineHeight); @@ -897,13 +944,32 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { contentWidth, _resultBody->height(), newWidth); - if (!_copy->isHidden()) { + if (authorVisible) { + y += _resultBody->height() + st::aiComposeAuthorLabelTop; + const auto authorWidth = contentWidth + - _copy->width() + - st::aiComposeCardControlSkip; + _authorLabel->resizeToWidth(authorWidth); + _authorLabel->setGeometryToLeft( + padding.left(), + y, + authorWidth, + _authorLabel->height(), + newWidth); _copy->moveToRight( padding.right(), - y + _resultBody->height() - lineHeight, + y + (_authorLabel->height() - _copy->height()) / 2, newWidth); + y += std::max(_authorLabel->height(), _copy->height()); + } else { + if (!_copy->isHidden()) { + _copy->moveToRight( + padding.right(), + y + _resultBody->height() - lineHeight, + newWidth); + } + y += _resultBody->height(); } - y += _resultBody->height(); return y + padding.bottom(); } @@ -1038,6 +1104,37 @@ void ComposeAiContent::setStyleTabs( _stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant); } +void ComposeAiContent::refreshTones() { + auto previousKey = QString(); + auto hadSelection = false; + if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) { + const auto &prev = _tones[_styleIndex]; + previousKey = prev.isDefault + ? prev.defaultType + : QString::number(prev.id); + hadSelection = true; + } + _tones = _session->data().aiComposeTones().list(); + _stylesData = ResolveStyleDescriptors(_tones); + _translateStylesData = ResolveTranslateStyleDescriptors( + _session, + _stylesData); + auto remapped = -1; + if (hadSelection) { + for (auto i = 0; i != int(_tones.size()); ++i) { + const auto &tone = _tones[i]; + const auto key = tone.isDefault + ? tone.defaultType + : QString::number(tone.id); + if (key == previousKey) { + remapped = i; + break; + } + } + } + _styleIndex = remapped; +} + void ComposeAiContent::start() { updatePinnedTabs(anim::type::instant); updateTitles(); @@ -1250,6 +1347,7 @@ void ComposeAiContent::request() { void ComposeAiContent::resetState(CardState state) { _state = state; _result = {}; + _preview->setAuthorId(UserId(0)); _preview->setState(state); notifyLoadingChanged(); updateTitles(); @@ -1270,6 +1368,13 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) { notifyLoadingChanged(); if (_state == CardState::Ready) { _preview->setResultText(std::move(display)); + if (_mode == ComposeAiMode::Style + && _styleIndex >= 0 + && _styleIndex < int(_tones.size())) { + _preview->setAuthorId(_tones[_styleIndex].authorId); + } else { + _preview->setAuthorId(UserId(0)); + } } updateTitles(); notifyReadyChanged(); @@ -1356,7 +1461,12 @@ bool ComposeAiContent::hasStyleSelection() const { return _styleIndex >= 0; } -[[nodiscard]] Fn SetupStyleTooltip( +struct StyleTooltipHandle { + QPointer tooltip; + Fn updateVisibility; +}; + +[[nodiscard]] StyleTooltipHandle SetupStyleTooltip( not_null box, not_null pinnedToTop, not_null stylesWrap, @@ -1429,7 +1539,7 @@ bool ComposeAiContent::hasStyleSelection() const { } }, tooltip->lifetime()); - return updateVisibility; + return { tooltip, updateVisibility }; } } // namespace @@ -1469,78 +1579,100 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { const auto content = body->add( object_ptr(box, box, args), st::aiComposeContentMargin); - auto emojiFactory = session->data().customEmojiManager().factory( - Data::CustomEmojiSizeTag::Large); - const auto stylesWrap = pinnedToTop->add( - object_ptr>( + const auto contextMenu = box->lifetime().make_state< + base::unique_qptr>(); + const auto stylesWrapHolder = box->lifetime().make_state< + QPointer>>(); + const auto styleTooltipHolder = box->lifetime().make_state< + QPointer>(); + const auto styleTooltipUpdater = box->lifetime().make_state< + Fn>(); + + content->setModeTabs(tabs); + + const auto rebuildStylesWrap = [=] { + if (const auto old = stylesWrapHolder->data()) { + delete old; + } + if (const auto old = styleTooltipHolder->data()) { + delete old; + } + auto emojiFactory = session->data().customEmojiManager().factory( + Data::CustomEmojiSizeTag::Large); + auto wrap = object_ptr>( pinnedToTop, object_ptr( pinnedToTop, WithAddStyleTab(content->stylesData()), std::move(emojiFactory)), - tabsSkip), - st::aiComposeContentMargin); - stylesWrap->hide(anim::type::instant); + tabsSkip); + const auto ptr = wrap.data(); + pinnedToTop->add(std::move(wrap), st::aiComposeContentMargin); + *stylesWrapHolder = ptr; + ptr->entity()->setContextMenuCallback([=](int index, QPoint globalPos) { + const auto &tones = content->tones(); + if (index < 0 || index >= int(tones.size())) { + return; + } + const auto &tone = tones[index]; + if (!tone.creator) { + return; + } + *contextMenu = base::make_unique_q( + ptr->entity(), + st::popupMenuWithIcons); + const auto toneCopy = tone; + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_edit(tr::now), + [=] { + box->uiShow()->show(Box( + EditAiToneBox, + session, + toneCopy, + nullptr)); + }, + &st::menuIconEdit); + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_share(tr::now), + [=] { + QGuiApplication::clipboard()->setText( + session->createInternalLinkFull( + "aistyle/" + toneCopy.slug)); + box->showToast( + tr::lng_ai_compose_tone_link_copied(tr::now)); + }, + &st::menuIconShare); + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_delete(tr::now), + [=] { + box->uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_delete_sure(), + .confirmed = [=](Fn &&close) { + close(); + session->data().aiComposeTones().remove(toneCopy); + }, + .confirmText = tr::lng_ai_compose_tone_delete(), + })); + }, + &st::menuIconDelete); + (*contextMenu)->popup(globalPos); + }); + content->setStyleTabs(ptr); + auto handle = SetupStyleTooltip( + box, + pinnedToTop, + ptr, + [=] { return content->mode(); }); + *styleTooltipHolder = handle.tooltip; + *styleTooltipUpdater = std::move(handle.updateVisibility); + }; + rebuildStylesWrap(); - const auto contextMenu = box->lifetime().make_state< - base::unique_qptr>(); - stylesWrap->entity()->setContextMenuCallback([=](int index, QPoint globalPos) { - const auto &tones = content->tones(); - if (index < 0 || index >= int(tones.size())) { - return; - } - const auto &tone = tones[index]; - if (!tone.creator) { - return; - } - *contextMenu = base::make_unique_q( - stylesWrap->entity(), - st::popupMenuWithIcons); - const auto toneCopy = tone; - (*contextMenu)->addAction( - tr::lng_ai_compose_tone_edit(tr::now), - [=] { - box->uiShow()->show(Box( - EditAiToneBox, - session, - toneCopy, - nullptr)); - }, - &st::menuIconEdit); - (*contextMenu)->addAction( - tr::lng_ai_compose_tone_share(tr::now), - [=] { - QGuiApplication::clipboard()->setText( - session->createInternalLinkFull( - "aistyle/" + toneCopy.slug)); - box->showToast( - tr::lng_ai_compose_tone_link_copied(tr::now)); - }, - &st::menuIconShare); - (*contextMenu)->addAction( - tr::lng_ai_compose_tone_delete(tr::now), - [=] { - box->uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_ai_compose_tone_delete_sure(), - .confirmed = [=](Fn &&close) { - close(); - session->data().aiComposeTones().remove(toneCopy); - }, - .confirmText = tr::lng_ai_compose_tone_delete(), - })); - }, - &st::menuIconDelete); - (*contextMenu)->popup(globalPos); - }); - - content->setModeTabs(tabs); - content->setStyleTabs(stylesWrap); - - const auto updateStyleTooltipVisibility = SetupStyleTooltip( - box, - pinnedToTop, - stylesWrap, - [=] { return content->mode(); }); + session->data().aiComposeTones().updated( + ) | rpl::on_next([=] { + content->refreshTones(); + rebuildStylesWrap(); + }, box->lifetime()); const auto sparkle = LoadingTitleSparkle(session); const auto loading = box->lifetime().make_state< @@ -1702,14 +1834,14 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { }); content->setModeChangedCallback([=](ComposeAiMode mode) { rebuildButtons(); - updateStyleTooltipVisibility(mode == ComposeAiMode::Style); + (*styleTooltipUpdater)(mode == ComposeAiMode::Style); }); content->setStyleSelectedCallback([=] { rebuildButtons(); if (!Core::App().settings().readPref(kAiComposeStyleTooltipHiddenPref)) { Core::App().settings().writePref(kAiComposeStyleTooltipHiddenPref, true); } - updateStyleTooltipVisibility(false); + (*styleTooltipUpdater)(false); }); rebuildButtons(); diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index 55c0462da1..9a217c6c65 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -7,28 +7,293 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/create_ai_tone_box.h" +#include "base/random.h" +#include "chat_helpers/compose/compose_show.h" +#include "chat_helpers/emoji_list_widget.h" +#include "chat_helpers/stickers_lottie.h" #include "data/data_ai_compose_tones.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_forum_icons.h" #include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/abstract_button.h" #include "ui/layers/generic_box.h" +#include "ui/painter.h" #include "ui/vertical_list.h" +#include "ui/widgets/shadow.h" #include "ui/widgets/fields/input_field.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" #include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_dialogs.h" #include "styles/style_layers.h" namespace { +void ChooseToneIconBox( + not_null box, + not_null controller, + Fn chosen) { + using namespace ChatHelpers; + + box->setTitle(tr::lng_ai_compose_tone_icon_title()); + box->setWidth(st::boxWideWidth); + box->setMaxHeight(st::editTopicMaxHeight); + box->setScrollStyle(st::reactPanelScroll); + + const auto manager = &controller->session().data().customEmojiManager(); + const auto icons = &controller->session().data().forumIcons(); + + auto factory = [=](DocumentId id, Fn repaint) + -> std::unique_ptr { + return manager->create( + id, + std::move(repaint), + Data::CustomEmojiManager::SizeTag::Large); + }; + + const auto top = box->setPinnedToTopContent( + object_ptr(box)); + + const auto body = box->verticalLayout(); + const auto selector = body->add( + object_ptr(body, EmojiListDescriptor{ + .show = controller->uiShow(), + .mode = EmojiListWidget::Mode::TopicIcon, + .paused = Window::PausedIn( + controller, + Window::GifPauseReason::Layer), + .customRecentList = DocumentListToRecent(icons->list()), + .customRecentFactory = std::move(factory), + .st = &st::reactPanelEmojiPan, + }), + st::reactPanelEmojiPan.padding); + + icons->requestDefaultIfUnknown(); + icons->defaultUpdates( + ) | rpl::on_next([=] { + selector->provideRecent(DocumentListToRecent(icons->list())); + }, selector->lifetime()); + + top->add(selector->createFooter()); + + const auto shadow = Ui::CreateChild(box.get()); + shadow->show(); + + rpl::combine( + top->heightValue(), + selector->widthValue() + ) | rpl::on_next([=](int topHeight, int width) { + shadow->setGeometry(0, topHeight, width, st::lineWidth); + }, shadow->lifetime()); + + selector->refreshEmoji(); + + selector->scrollToRequests( + ) | rpl::on_next([=](int y) { + box->scrollToY(y); + shadow->update(); + }, selector->lifetime()); + + rpl::combine( + box->heightValue(), + top->heightValue(), + rpl::mappers::_1 - rpl::mappers::_2 + ) | rpl::on_next([=](int height) { + selector->setMinimalHeight(selector->width(), height); + }, body->lifetime()); + + selector->customChosen( + ) | rpl::on_next([=](ChatHelpers::FileChosen data) { + chosen(data.document->id); + box->closeBox(); + }, selector->lifetime()); + + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +void AddIconPreview( + not_null container, + not_null session, + rpl::producer emojiIdValue, + Fn emojiIdChosen) { + using StickerPlayer = HistoryView::StickerPlayer; + struct State { + DocumentId emojiId = 0; + std::shared_ptr player; + bool playerUsesTextColor = false; + }; + + const auto outer = st::aiToneIconPreviewSize; + const auto inner = st::aiToneIconPreviewInnerSize; + const auto top = st::aiToneIconPreviewTopSkip; + const auto bottom = st::aiToneIconPreviewBottomSkip; + const auto holder = container->add( + object_ptr( + container, + outer + top + bottom)); + const auto button = Ui::CreateChild(holder); + button->resize(outer, outer); + button->show(); + + holder->widthValue( + ) | rpl::on_next([=](int width) { + button->move((width - outer) / 2, top); + }, button->lifetime()); + + const auto state = button->lifetime().make_state(); + const auto emojiIdVar = button->lifetime().make_state< + rpl::variable>(std::move(emojiIdValue)); + + emojiIdVar->value( + ) | rpl::on_next([=](DocumentId id) { + state->emojiId = id; + }, button->lifetime()); + + const auto icons = &session->data().forumIcons(); + icons->requestDefaultIfUnknown(); + const auto seedRandom = [=] { + if (state->emojiId) { + return; + } + const auto &list = icons->list(); + if (list.empty()) { + return; + } + emojiIdChosen(list[base::RandomIndex(list.size())]); + }; + seedRandom(); + icons->defaultUpdates( + ) | rpl::on_next(seedRandom, button->lifetime()); + + emojiIdVar->value( + ) | rpl::map([=](DocumentId id) -> rpl::producer { + if (!id) { + return rpl::single((DocumentData*)nullptr); + } + return session->data().customEmojiManager().resolve( + id + ) | rpl::map([=](not_null document) { + return document.get(); + }) | rpl::map_error_to_done(); + }) | rpl::flatten_latest( + ) | rpl::map([=](DocumentData *document) + -> rpl::producer> { + if (!document) { + return rpl::single(std::shared_ptr()); + } + const auto media = document->createMediaView(); + media->checkStickerLarge(); + media->goodThumbnailWanted(); + + return rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([=] { + return media->loaded(); + }) | rpl::take(1) | rpl::map([=] { + auto result = std::shared_ptr(); + const auto sticker = document->sticker(); + const auto size = QSize(inner, inner); + if (sticker && sticker->isLottie()) { + result = std::make_shared( + ChatHelpers::LottiePlayerFromDocument( + media.get(), + ChatHelpers::StickerLottieSize::StickerSet, + size, + Lottie::Quality::High)); + } else if (sticker && sticker->isWebm()) { + result = std::make_shared( + media->owner()->location(), + media->bytes(), + size); + } else { + result = std::make_shared( + media->owner()->location(), + media->bytes(), + size); + } + result->setRepaintCallback([=] { button->update(); }); + state->playerUsesTextColor + = media->owner()->emojiUsesTextColor(); + return result; + }); + }) | rpl::flatten_latest( + ) | rpl::on_next([=](std::shared_ptr player) { + state->player = std::move(player); + button->update(); + }, button->lifetime()); + + button->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(button); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::aiToneIconPreviewBg); + p.drawEllipse(button->rect()); + if (state->player && state->player->ready()) { + const auto color = state->playerUsesTextColor + ? st::windowFg->c + : QColor(0, 0, 0, 0); + const auto frame = state->player->frame( + QSize(inner, inner), + color, + false, + crl::now(), + false).image; + const auto sz = frame.size() / style::DevicePixelRatio(); + p.drawImage( + QRect( + (outer - sz.width()) / 2, + (outer - sz.height()) / 2, + sz.width(), + sz.height()), + frame); + state->player->markFrameShown(); + } + }, button->lifetime()); + + button->setClickedCallback([=] { + const auto controller = ChatHelpers::ResolveWindowDefault()( + session); + if (!controller) { + return; + } + controller->uiShow()->showBox(Box( + ChooseToneIconBox, + controller, + crl::guard(button, [=](DocumentId id) { + emojiIdChosen(id); + }))); + }); +} + void SetupToneBox( not_null box, not_null session, + DocumentId initialEmojiId, const QString &initialName, const QString &initialPrompt, rpl::producer title, - Fn submit) { + Fn submit) { box->setTitle(std::move(title)); + const auto container = box->verticalLayout(); + const auto emojiId = container->lifetime().make_state< + rpl::variable>(initialEmojiId); + + AddIconPreview( + container, + session, + emojiId->value(), + [=](DocumentId id) { *emojiId = id; }); + const auto name = box->addRow(object_ptr( box, st::defaultInputField, @@ -62,7 +327,7 @@ void SetupToneBox( } return; } - submit(nameText, promptText); + submit(emojiId->current(), nameText, promptText); }; box->addButton(tr::lng_ai_compose_tone_save(), save); @@ -78,14 +343,17 @@ void CreateAiToneBox( SetupToneBox( box, session, + DocumentId(0), QString(), QString(), tr::lng_ai_compose_create_tone_title(), - [=](const QString &name, const QString &prompt) { + [=](DocumentId emojiId, + const QString &name, + const QString &prompt) { session->data().aiComposeTones().create( name, prompt, - 0, + emojiId, false, [=](Data::AiComposeTone) { box->closeBox(); @@ -106,10 +374,13 @@ void EditAiToneBox( SetupToneBox( box, session, + tone.emojiId, tone.title, tone.prompt, tr::lng_ai_compose_edit_tone_title(), - [=](const QString &name, const QString &prompt) { + [=](DocumentId emojiId, + const QString &name, + const QString &prompt) { auto toneCopy = Data::AiComposeTone(); toneCopy.id = toneId; toneCopy.accessHash = toneAccessHash; @@ -117,7 +388,7 @@ void EditAiToneBox( toneCopy, name, prompt, - std::nullopt, + std::make_optional(emojiId), std::nullopt, [=](Data::AiComposeTone) { box->closeBox(); diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp index 1c2b253d5e..438a6870e8 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -7,8 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_ai_compose_tones.h" -#include "main/main_session.h" #include "apiwrap.h" +#include "data/data_session.h" +#include "main/main_session.h" namespace Data { namespace { @@ -33,6 +34,7 @@ void AiComposeTones::refresh() { )).done([=](const MTPaicompose_Tones &result) { _refreshRequestId = 0; result.match([&](const MTPDaicompose_tones &data) { + _session->data().processUsers(data.vusers()); _hash = data.vhash().v; parseTones(data.vtones().v); _updates.fire({}); @@ -60,7 +62,7 @@ AiComposeTone AiComposeTones::parseTone( .slug = qs(data.vslug()), .title = qs(data.vtitle()), .emojiId = data.vemoji_id().value_or_empty(), - .prompt = qs(data.vprompt()), + .prompt = qs(data.vprompt().value_or_empty()), .installsCount = data.vinstalls_count().value_or_empty(), .authorId = data.vauthor_id() ? UserId(data.vauthor_id()->v) @@ -99,6 +101,9 @@ void AiComposeTones::create( MTP_string(prompt) )).done([=](const MTPAiComposeTone &result) { auto parsed = parseTone(result); + _list.push_back(parsed); + _hash = 0; + _updates.fire({}); if (done) { done(parsed); } @@ -143,6 +148,14 @@ void AiComposeTones::update( MTP_string(prompt.value_or(QString())) )).done([=](const MTPAiComposeTone &result) { auto parsed = parseTone(result); + const auto i = ranges::find(_list, parsed.id, &AiComposeTone::id); + if (i != end(_list)) { + *i = parsed; + } else { + _list.push_back(parsed); + } + _hash = 0; + _updates.fire({}); if (done) { done(parsed); } @@ -173,9 +186,21 @@ void AiComposeTones::save( void AiComposeTones::remove( const AiComposeTone &tone, Fn done) { + const auto toneCopy = tone; _session->api().request(MTPaicompose_DeleteTone( toneToMTP(tone) )).done([=] { + if (!toneCopy.isDefault) { + const auto i = ranges::find( + _list, + toneCopy.id, + &AiComposeTone::id); + if (i != end(_list)) { + _list.erase(i); + } + } + _hash = 0; + _updates.fire({}); if (done) { done(); } @@ -190,10 +215,26 @@ void AiComposeTones::resolve( Fn fail) { _session->api().request(MTPaicompose_GetTone( MTP_inputAiComposeToneSlug(MTP_string(slug)) - )).done([=](const MTPAiComposeTone &result) { - if (done) { - done(parseTone(result)); - } + )).done([=](const MTPaicompose_Tones &result) { + result.match([&](const MTPDaicompose_tones &data) { + _session->data().processUsers(data.vusers()); + const auto &tones = data.vtones().v; + if (!tones.isEmpty()) { + if (done) { + done(parseTone(tones.front())); + } + } else if (fail) { + fail(MTP::Error::Local( + "TONE_NOT_FOUND", + "Tone not found.")); + } + }, [&](const MTPDaicompose_tonesNotModified &) { + if (fail) { + fail(MTP::Error::Local( + "TONE_NOT_MODIFIED", + "Tone not modified.")); + } + }); }).fail([=](const MTP::Error &error) { if (fail) { fail(error); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 192259589a..887525bf18 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -2163,11 +2163,13 @@ inputAiComposeToneDefault#1fe9a9bf tone:string = InputAiComposeTone; inputAiComposeToneID#773c080 id:long access_hash:long = InputAiComposeTone; inputAiComposeToneSlug#1fa01357 slug:string = InputAiComposeTone; -aiComposeTone#12ea1465 flags:# creator:flags.0?true id:long access_hash:long slug:string title:string emoji_id:flags.1?long prompt:string installs_count:flags.2?int author_id:flags.3?long = AiComposeTone; +aiComposeTone#cff63ea9 flags:# creator:flags.0?true id:long access_hash:long slug:string title:string emoji_id:flags.1?long prompt:flags.4?string installs_count:flags.2?int author_id:flags.3?long example_english:flags.5?AiComposeToneExample = AiComposeTone; aiComposeToneDefault#9bad6414 tone:string emoji_id:long title:string = AiComposeTone; aicompose.tonesNotModified#c1f46103 = aicompose.Tones; -aicompose.tones#65175942 hash:long tones:Vector = aicompose.Tones; +aicompose.tones#6c9d0efe hash:long tones:Vector users:Vector = aicompose.Tones; + +aiComposeToneExample#a8dc3b99 from:string to:string = AiComposeToneExample; ---functions--- @@ -2972,7 +2974,8 @@ aicompose.createTone#70f76f8 flags:# display_author:flags.0?true emoji_id:flags. aicompose.updateTone#903bcf59 flags:# tone:InputAiComposeTone display_author:flags.0?Bool emoji_id:flags.1?long title:flags.2?string prompt:flags.3?string = AiComposeTone; aicompose.saveTone#1782cbb1 tone:InputAiComposeTone unsave:Bool = Bool; aicompose.deleteTone#dd39316a tone:InputAiComposeTone = Bool; -aicompose.getTone#4679e1df tone:InputAiComposeTone = AiComposeTone; +aicompose.getTone#b2e8ba03 tone:InputAiComposeTone = aicompose.Tones; aicompose.getTones#abd59201 hash:long = aicompose.Tones; +aicompose.getToneExample#d1b4ab14 tone:InputAiComposeTone num:int = AiComposeToneExample; // LAYER 225 From 48f4e0f360eab39f89efcb8a55e4b084f6d0b34c Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 16 Apr 2026 21:56:44 +0700 Subject: [PATCH 092/154] Add icon for new tone icon placeholder. --- Telegram/Resources/icons/chat/ai_style_tone.svg | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Telegram/Resources/icons/chat/ai_style_tone.svg diff --git a/Telegram/Resources/icons/chat/ai_style_tone.svg b/Telegram/Resources/icons/chat/ai_style_tone.svg new file mode 100644 index 0000000000..778574b679 --- /dev/null +++ b/Telegram/Resources/icons/chat/ai_style_tone.svg @@ -0,0 +1,10 @@ + + + + + + + + + + From 776ea0d6aa466abb52485b1e3692d870a79e303f Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 17 Apr 2026 10:22:38 +0700 Subject: [PATCH 093/154] Improve Create/Edit custom AI tone box design. --- Telegram/Resources/langs/lang.strings | 5 +- Telegram/SourceFiles/boxes/boxes.style | 38 +++- Telegram/SourceFiles/boxes/compose_ai_box.cpp | 2 +- .../SourceFiles/boxes/create_ai_tone_box.cpp | 214 ++++++++++++++---- 4 files changed, 217 insertions(+), 42 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f4c17030bd..4da4aa18fa 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7919,13 +7919,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_select_style" = "Select Style"; "lng_ai_compose_apply_style" = "Apply Style"; "lng_ai_compose_style_tooltip" = "Choose Style"; -"lng_ai_compose_add_style" = "Add Style"; "lng_ai_compose_create_tone_title" = "New Style"; "lng_ai_compose_edit_tone_title" = "Edit Style"; "lng_ai_compose_tone_icon_title" = "Style Icon"; "lng_ai_compose_tone_name" = "Name"; "lng_ai_compose_tone_prompt" = "Prompt"; "lng_ai_compose_tone_save" = "Save"; +"lng_ai_compose_tone_create" = "Create"; +"lng_ai_compose_tone_author" = "Add a link to my account"; +"lng_ai_compose_tone_name_placeholder" = "Style Name (for example, \"Pirate\")"; +"lng_ai_compose_tone_prompt_placeholder" = "Instructions (for example \"write in bold, nautical tone, light slang (aye, matey), vivid sea imagery, playful swagger, rhythmic phrasing, and adventurous mood\")"; "lng_ai_compose_tone_edit" = "Edit"; "lng_ai_compose_tone_share" = "Share"; "lng_ai_compose_tone_delete" = "Delete"; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index a12102956e..0920945be4 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1252,9 +1252,43 @@ aiComposeAddStyleIconOver: icon {{ "menu/edit_stars_add", aiComposeButtonFgActiv aiToneIconPreviewSize: 80px; aiToneIconPreviewBottomSkip: 12px; aiToneIconPreviewTopSkip: 4px; -aiToneIconPreviewBg: windowBgOver; +aiToneIconPreviewBg: boxBg; aiToneIconPreviewInnerSize: 54px; +aiToneIconPreviewPlaceholder: icon {{ "chat/ai_style_tone", windowSubTextFg }}; + +aiToneFieldBg: boxBg; +aiToneFieldRadius: 12px; +aiToneFieldPadding: margins(16px, 12px, 16px, 12px); +aiToneFieldsMargin: margins(16px, 0px, 16px, 0px); +aiToneFieldsSkip: 8px; + +aiToneNameField: InputField(defaultInputField) { + textBg: transparent; + textBgActive: transparent; + textMargins: margins(16px, 12px, 16px, 12px); + border: 0px; + borderActive: 0px; + heightMin: 44px; +} +aiTonePromptField: InputField(newGroupDescription) { + textBg: transparent; + textBgActive: transparent; + textMargins: margins(16px, 12px, 16px, 12px); + border: 0px; + borderActive: 0px; + heightMin: 140px; + heightMax: 240px; +} +aiTonePlaceholderLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 80px; + style: TextStyle(defaultTextStyle) { + font: font(14px); + } +} +aiToneAuthorCheckboxMargin: margins(0px, 12px, 0px, 8px); + aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; aiComposeCardPadding: margins(12px, 16px, 16px, 16px); @@ -1267,6 +1301,8 @@ aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) { maxHeight: 22px; } aiComposeEmojifyCheckbox: Checkbox(defaultBoxCheckbox) { + textFg: windowSubTextFg; + textFgActive: windowSubTextFg; width: 0px; } diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 076f5a52c0..7ef8e3079d 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -261,7 +261,7 @@ enum class CardState { -> std::vector { tabs.push_back({ .id = u"_add_style"_q, - .label = tr::lng_ai_compose_add_style(tr::now), + .label = tr::lng_ai_compose_tone_create(tr::now), .icon = &st::aiComposeAddStyleIcon, .iconActive = &st::aiComposeAddStyleIconOver, }); diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index 9a217c6c65..fc4cc72326 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/create_ai_tone_box.h" -#include "base/random.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/emoji_list_widget.h" #include "chat_helpers/stickers_lottie.h" @@ -21,11 +20,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/abstract_button.h" +#include "ui/effects/animations.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/vertical_list.h" -#include "ui/widgets/shadow.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" @@ -157,22 +159,6 @@ void AddIconPreview( state->emojiId = id; }, button->lifetime()); - const auto icons = &session->data().forumIcons(); - icons->requestDefaultIfUnknown(); - const auto seedRandom = [=] { - if (state->emojiId) { - return; - } - const auto &list = icons->list(); - if (list.empty()) { - return; - } - emojiIdChosen(list[base::RandomIndex(list.size())]); - }; - seedRandom(); - icons->defaultUpdates( - ) | rpl::on_next(seedRandom, button->lifetime()); - emojiIdVar->value( ) | rpl::map([=](DocumentId id) -> rpl::producer { if (!id) { @@ -256,6 +242,10 @@ void AddIconPreview( sz.height()), frame); state->player->markFrameShown(); + } else if (!state->emojiId) { + st::aiToneIconPreviewPlaceholder.paintInCenter( + p, + button->rect()); } }, button->lifetime()); @@ -280,8 +270,14 @@ void SetupToneBox( DocumentId initialEmojiId, const QString &initialName, const QString &initialPrompt, + bool initialDisplayAuthor, rpl::producer title, - Fn submit) { + rpl::producer submitLabel, + Fn submit) { + box->setStyle(st::aiComposeBox); + box->setNoContentMargin(true); + box->setWidth(st::boxWideWidth); + box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); }); box->setTitle(std::move(title)); const auto container = box->verticalLayout(); @@ -294,22 +290,152 @@ void SetupToneBox( emojiId->value(), [=](DocumentId id) { *emojiId = id; }); - const auto name = box->addRow(object_ptr( - box, - st::defaultInputField, - tr::lng_ai_compose_tone_name(), - initialName)); + const auto name = box->addRow( + object_ptr( + box, + st::aiToneNameField, + Ui::InputField::Mode::SingleLine, + rpl::producer(), + initialName), + st::aiToneFieldsMargin); - Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(container, st::aiToneFieldsSkip); const auto promptSt = box->lifetime().make_state( - st::newGroupDescription); - const auto prompt = box->addRow(object_ptr( - box, - *promptSt, - Ui::InputField::Mode::MultiLine, - tr::lng_ai_compose_tone_prompt(), - initialPrompt)); + st::aiTonePromptField); + { + const auto &placeholderStyle = st::aiTonePlaceholderLabel.style; + const auto fieldsMargin = st::aiToneFieldsMargin; + const auto contentWidth = st::boxWideWidth + - fieldsMargin.left() - fieldsMargin.right() + - promptSt->textMargins.left() - promptSt->textMargins.right(); + auto measure = Ui::Text::String{ contentWidth / 2 }; + measure.setText( + placeholderStyle, + tr::lng_ai_compose_tone_prompt_placeholder(tr::now)); + const auto desiredMin = measure.countHeight(contentWidth) + + promptSt->textMargins.top() + + promptSt->textMargins.bottom(); + if (promptSt->heightMin < desiredMin) { + promptSt->heightMin = desiredMin; + } + if (promptSt->heightMax < promptSt->heightMin) { + promptSt->heightMax = promptSt->heightMin; + } + } + + const auto prompt = box->addRow( + object_ptr( + box, + *promptSt, + Ui::InputField::Mode::MultiLine, + rpl::producer(), + initialPrompt), + st::aiToneFieldsMargin); + prompt->setSubmitSettings(Ui::InputField::SubmitSettings::None); + + struct FieldDecor { + not_null bg; + not_null placeholder; + Ui::Animations::Simple anim; + bool hidden = false; + }; + const auto makeDecor = [=]( + not_null field, + rpl::producer placeholderText) { + const auto parent = field->parentWidget(); + const auto decor = field->lifetime().make_state(FieldDecor{ + .bg = Ui::CreateChild(parent), + .placeholder = Ui::CreateChild( + parent, + std::move(placeholderText), + st::aiTonePlaceholderLabel), + }); + decor->bg->setAttribute(Qt::WA_TransparentForMouseEvents); + decor->placeholder->setAttribute(Qt::WA_TransparentForMouseEvents); + decor->bg->paintRequest( + ) | rpl::on_next([bg = decor->bg] { + auto p = QPainter(bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::aiToneFieldBg); + const auto r = st::aiToneFieldRadius; + p.drawRoundedRect(bg->rect(), r, r); + }, decor->bg->lifetime()); + decor->bg->lower(); + decor->placeholder->raise(); + + const auto applyPosition = [=] { + const auto pad = st::aiToneFieldPadding; + const auto progress = decor->anim.value(decor->hidden ? 1. : 0.); + const auto shift = int(base::SafeRound( + progress * (-st::defaultInputField.placeholderShift))); + decor->placeholder->moveToLeft( + field->x() + pad.left() + shift, + field->y() + pad.top()); + decor->placeholder->setOpacity(1. - progress); + }; + field->geometryValue( + ) | rpl::on_next([=](QRect g) { + if (g.isEmpty()) { + return; + } + const auto pad = st::aiToneFieldPadding; + decor->bg->setGeometry(g); + decor->placeholder->resizeToWidth( + g.width() - pad.left() - pad.right()); + applyPosition(); + }, field->lifetime()); + + const auto animate = [=](bool hidden) { + if (decor->hidden == hidden) { + return; + } + decor->hidden = hidden; + decor->anim.start( + applyPosition, + hidden ? 0. : 1., + hidden ? 1. : 0., + st::defaultInputField.duration); + }; + field->changes( + ) | rpl::on_next([=] { + animate(!field->getLastText().isEmpty()); + }, field->lifetime()); + decor->hidden = !field->getLastText().isEmpty(); + applyPosition(); + return decor; + }; + makeDecor(name, tr::lng_ai_compose_tone_name_placeholder()); + const auto promptDecor = makeDecor( + prompt, + tr::lng_ai_compose_tone_prompt_placeholder()); + + const auto authorCheckbox = box->addRow( + object_ptr( + box, + tr::lng_ai_compose_tone_author(tr::now), + st::aiComposeEmojifyCheckbox, + std::make_unique( + st::defaultCheck, + initialDisplayAuthor)), + st::aiToneAuthorCheckboxMargin, + style::al_top); + + rpl::combine( + prompt->topValue(), + promptDecor->placeholder->heightValue(), + box->getDelegate()->contentHeightMaxValue() + ) | rpl::on_next([=](int top, int phHeight, int contentHeight) { + const auto pad = st::aiToneFieldPadding; + prompt->setMaxHeight(contentHeight + - top + - st::aiToneFieldsMargin.bottom() + - authorCheckbox->heightNoMargins() + - st::aiToneAuthorCheckboxMargin.top() + - st::aiToneAuthorCheckboxMargin.bottom()); + prompt->setMinHeight(phHeight + pad.top() + pad.bottom()); + }, prompt->lifetime()); box->setFocusCallback([=] { name->setFocusFast(); @@ -327,11 +453,15 @@ void SetupToneBox( } return; } - submit(emojiId->current(), nameText, promptText); + submit( + emojiId->current(), + nameText, + promptText, + authorCheckbox->checked()); }; - box->addButton(tr::lng_ai_compose_tone_save(), save); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + const auto submitBtn = box->addButton(std::move(submitLabel), save); + submitBtn->setFullRadius(true); } } // namespace @@ -346,15 +476,18 @@ void CreateAiToneBox( DocumentId(0), QString(), QString(), + false, tr::lng_ai_compose_create_tone_title(), + tr::lng_ai_compose_tone_create(), [=](DocumentId emojiId, const QString &name, - const QString &prompt) { + const QString &prompt, + bool displayAuthor) { session->data().aiComposeTones().create( name, prompt, emojiId, - false, + displayAuthor, [=](Data::AiComposeTone) { box->closeBox(); if (saved) { @@ -377,10 +510,13 @@ void EditAiToneBox( tone.emojiId, tone.title, tone.prompt, + tone.authorId != 0, tr::lng_ai_compose_edit_tone_title(), + tr::lng_ai_compose_tone_save(), [=](DocumentId emojiId, const QString &name, - const QString &prompt) { + const QString &prompt, + bool displayAuthor) { auto toneCopy = Data::AiComposeTone(); toneCopy.id = toneId; toneCopy.accessHash = toneAccessHash; @@ -389,7 +525,7 @@ void EditAiToneBox( name, prompt, std::make_optional(emojiId), - std::nullopt, + std::make_optional(displayAuthor), [=](Data::AiComposeTone) { box->closeBox(); if (saved) { From d96b73ca00d764edfea88c50b53e1f54cf8ee29e Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 17 Apr 2026 12:58:01 +0700 Subject: [PATCH 094/154] Update API scheme on layer 225. Show toasts. --- Telegram/CMakeLists.txt | 4 + Telegram/Resources/langs/lang.strings | 6 + Telegram/SourceFiles/boxes/boxes.style | 2 + Telegram/SourceFiles/boxes/compose_ai_box.cpp | 33 +++++- .../SourceFiles/boxes/create_ai_tone_box.cpp | 90 +++++++++++--- .../SourceFiles/boxes/create_ai_tone_box.h | 4 +- .../data/data_ai_compose_tones.cpp | 3 - .../SourceFiles/data/data_file_origin.cpp | 1 + Telegram/SourceFiles/data/data_session.cpp | 2 + Telegram/SourceFiles/mtproto/scheme/api.tl | 7 +- .../ui/controls/custom_emoji_toast_icon.cpp | 57 +++++++++ .../ui/controls/custom_emoji_toast_icon.h | 27 +++++ .../ui/controls/warning_tooltip.cpp | 112 ++++++++++++++++++ .../SourceFiles/ui/controls/warning_tooltip.h | 58 +++++++++ 14 files changed, 383 insertions(+), 23 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.cpp create mode 100644 Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.h create mode 100644 Telegram/SourceFiles/ui/controls/warning_tooltip.cpp create mode 100644 Telegram/SourceFiles/ui/controls/warning_tooltip.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index fdde7123e2..43684ca368 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1777,6 +1777,8 @@ PRIVATE ui/chat/sponsored_message_bar.h ui/controls/compose_ai_button_factory.cpp ui/controls/compose_ai_button_factory.h + ui/controls/custom_emoji_toast_icon.cpp + ui/controls/custom_emoji_toast_icon.h ui/controls/emoji_button_factory.cpp ui/controls/emoji_button_factory.h ui/controls/location_picker.cpp @@ -1787,6 +1789,8 @@ PRIVATE ui/controls/table_rows.h ui/controls/userpic_button.cpp ui/controls/userpic_button.h + ui/controls/warning_tooltip.cpp + ui/controls/warning_tooltip.h ui/effects/credits_graphics.cpp ui/effects/credits_graphics.h ui/effects/emoji_fly_animation.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4da4aa18fa..67ecbc536e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7937,6 +7937,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_saved" = "Style saved."; "lng_ai_compose_tone_link_copied" = "Style link copied."; "lng_ai_compose_author" = "This style was created by {user}"; +"lng_ai_compose_tone_warn_icon" = "Please choose an icon."; +"lng_ai_compose_tone_warn_name" = "Please choose a name."; +"lng_ai_compose_tone_warn_prompt" = "Please enter instructions."; +"lng_ai_compose_tone_created" = "{title} Style Created!"; +"lng_ai_compose_tone_updated" = "{title} Style Updated!"; +"lng_ai_compose_tone_created_description" = "Right click the style to edit or share the link."; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 0920945be4..f69bf8b671 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1288,6 +1288,8 @@ aiTonePlaceholderLabel: FlatLabel(defaultFlatLabel) { } } aiToneAuthorCheckboxMargin: margins(0px, 12px, 0px, 8px); +aiComposeToneToastIconSize: size(32px, 32px); +aiComposeToneToastIconPadding: margins(12px, 6px, 12px, 6px); aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 7ef8e3079d..ef37a8f1fc 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -398,6 +398,7 @@ public: void setModeTabs(not_null tabs); void setStyleTabs(not_null*> stylesWrap); void refreshTones(); + void selectToneById(uint64 id); void start(); protected: @@ -1097,7 +1098,12 @@ void ComposeAiContent::setStyleTabs( } } else if (index == int(_tones.size())) { _styles->setActive(_styleIndex); - _box->uiShow()->show(Box(CreateAiToneBox, _session, nullptr)); + _box->uiShow()->show(Box( + CreateAiToneBox, + _session, + crl::guard(this, [=](Data::AiComposeTone tone) { + selectToneById(tone.id); + }))); } }); _styles->setActive(_styleIndex); @@ -1135,6 +1141,27 @@ void ComposeAiContent::refreshTones() { _styleIndex = remapped; } +void ComposeAiContent::selectToneById(uint64 id) { + for (auto i = 0; i != int(_tones.size()); ++i) { + const auto &tone = _tones[i]; + if (!tone.isDefault && tone.id == id) { + const auto wasNoSelection = (_styleIndex < 0); + _styleIndex = i; + updateTitles(); + if (_styles) { + _styles->setActive(_styleIndex); + } + if (_mode == ComposeAiMode::Style) { + request(); + if (wasNoSelection && _styleSelected) { + _styleSelected(); + } + } + return; + } + } +} + void ComposeAiContent::start() { updatePinnedTabs(anim::type::instant); updateTitles(); @@ -1629,7 +1656,9 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { EditAiToneBox, session, toneCopy, - nullptr)); + crl::guard(content, [=](Data::AiComposeTone tone) { + content->selectToneById(tone.id); + }))); }, &st::menuIconEdit); (*contextMenu)->addAction( diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index fc4cc72326..d0d6b0ce79 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -20,9 +20,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/abstract_button.h" +#include "ui/controls/custom_emoji_toast_icon.h" +#include "ui/controls/warning_tooltip.h" #include "ui/effects/animations.h" #include "ui/layers/generic_box.h" +#include "ui/layers/show.h" #include "ui/painter.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" @@ -39,6 +44,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +constexpr auto kAiComposeToneToastDuration = crl::time(4000); + +void ShowToneToast( + std::shared_ptr show, + not_null session, + const Data::AiComposeTone &tone, + bool created) { + const auto size = QSize( + st::aiComposeToneToastIconSize.width(), + st::aiComposeToneToastIconSize.height()); + show->showToast(Ui::Toast::Config{ + .title = (created + ? tr::lng_ai_compose_tone_created + : tr::lng_ai_compose_tone_updated)( + tr::now, + lt_title, + tone.title), + .text = tr::lng_ai_compose_tone_created_description( + tr::now, + Ui::Text::WithEntities), + .iconContent = Ui::MakeCustomEmojiToastIcon( + session, + tone.emojiId, + size), + .iconPadding = st::aiComposeToneToastIconPadding, + .duration = kAiComposeToneToastDuration, + }); +} + void ChooseToneIconBox( not_null box, not_null controller, @@ -121,7 +155,7 @@ void ChooseToneIconBox( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } -void AddIconPreview( +not_null AddIconPreview( not_null container, not_null session, rpl::producer emojiIdValue, @@ -262,6 +296,8 @@ void AddIconPreview( emojiIdChosen(id); }))); }); + + return button; } void SetupToneBox( @@ -284,7 +320,7 @@ void SetupToneBox( const auto emojiId = container->lifetime().make_state< rpl::variable>(initialEmojiId); - AddIconPreview( + const auto iconButton = AddIconPreview( container, session, emojiId->value(), @@ -441,18 +477,38 @@ void SetupToneBox( name->setFocusFast(); }); + const auto warning = box->lifetime().make_state(); const auto save = [=] { const auto nameText = name->getLastText().trimmed(); const auto promptText = prompt->getLastText().trimmed(); - if (nameText.isEmpty() || promptText.isEmpty()) { - if (nameText.isEmpty()) { - name->showError(); - } - if (promptText.isEmpty()) { - prompt->showError(); - } + const auto showWarning = [=]( + not_null target, + rpl::producer text) { + warning->show({ + .parent = box, + .target = target, + .text = std::move(text), + }); + }; + if (!emojiId->current()) { + showWarning( + iconButton, + tr::lng_ai_compose_tone_warn_icon(tr::marked)); + return; + } else if (nameText.isEmpty()) { + name->showError(); + showWarning( + name, + tr::lng_ai_compose_tone_warn_name(tr::marked)); + return; + } else if (promptText.isEmpty()) { + prompt->showError(); + showWarning( + prompt, + tr::lng_ai_compose_tone_warn_prompt(tr::marked)); return; } + warning->hide(anim::type::normal); submit( emojiId->current(), nameText, @@ -469,7 +525,7 @@ void SetupToneBox( void CreateAiToneBox( not_null box, not_null session, - Fn saved) { + Fn saved) { SetupToneBox( box, session, @@ -488,10 +544,12 @@ void CreateAiToneBox( prompt, emojiId, displayAuthor, - [=](Data::AiComposeTone) { + [=](Data::AiComposeTone tone) { + const auto show = box->uiShow(); box->closeBox(); + ShowToneToast(show, session, tone, true); if (saved) { - saved(); + saved(tone); } }); }); @@ -501,7 +559,7 @@ void EditAiToneBox( not_null box, not_null session, const Data::AiComposeTone &tone, - Fn saved) { + Fn saved) { const auto toneId = tone.id; const auto toneAccessHash = tone.accessHash; SetupToneBox( @@ -526,10 +584,12 @@ void EditAiToneBox( prompt, std::make_optional(emojiId), std::make_optional(displayAuthor), - [=](Data::AiComposeTone) { + [=](Data::AiComposeTone updated) { + const auto show = box->uiShow(); box->closeBox(); + ShowToneToast(show, session, updated, false); if (saved) { - saved(); + saved(updated); } }); }); diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h index cfcbf1647e..01730aa33e 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.h +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -22,10 +22,10 @@ class GenericBox; void CreateAiToneBox( not_null box, not_null session, - Fn saved = nullptr); + Fn saved = nullptr); void EditAiToneBox( not_null box, not_null session, const Data::AiComposeTone &tone, - Fn saved = nullptr); + Fn saved = nullptr); diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp index 438a6870e8..6b3b4dc616 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -91,9 +91,6 @@ void AiComposeTones::create( if (displayAuthor) { flags |= Flag::f_display_author; } - if (emojiId) { - flags |= Flag::f_emoji_id; - } _session->api().request(MTPaicompose_CreateTone( MTP_flags(flags), MTP_long(emojiId), diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index 9a832c36f3..505d6bb74a 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -65,6 +65,7 @@ struct FileReferenceAccumulator { push(data.vicons()); }, [&](const MTPDwebPageAttributeStarGiftAuction &data) { push(data.vgift()); + }, [](const MTPDwebPageAttributeAiComposeTone &) { }); } void push(const MTPStarGift &data) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 96eadb153d..ebd2db5b37 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4012,6 +4012,8 @@ void Session::webpageApplyFields( return (DocumentData*)nullptr; }, [](const MTPDwebPageAttributeStarGiftAuction &) { return (DocumentData*)nullptr; + }, [](const MTPDwebPageAttributeAiComposeTone &) { + return (DocumentData*)nullptr; }); if (result) { return result; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 887525bf18..5334c9166d 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1324,6 +1324,7 @@ webPageAttributeStickerSet#50cc03d3 flags:# emojis:flags.0?true text_color:flags webPageAttributeUniqueStarGift#cf6f6db8 gift:StarGift = WebPageAttribute; webPageAttributeStarGiftCollection#31cad303 icons:Vector = WebPageAttribute; webPageAttributeStarGiftAuction#1c641c2 gift:StarGift end_date:int = WebPageAttribute; +webPageAttributeAiComposeTone#7781fe18 emoji_id:long = WebPageAttribute; messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; @@ -2171,6 +2172,8 @@ aicompose.tones#6c9d0efe hash:long tones:Vector users:Vector = bots.AccessSettings; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2768,6 +2771,8 @@ bots.createBot#e5b17f2b flags:# via_deeplink:flags.0?true name:string username:s bots.exportBotToken#bd0d99eb bot:InputUser revoke:Bool = bots.ExportedBotToken; bots.requestWebViewButton#31a2a35e user_id:InputUser button:KeyboardButton = bots.RequestedButton; bots.getRequestedWebViewButton#bf25b7f3 bot:InputUser webapp_req_id:string = KeyboardButton; +bots.getAccessSettings#213853a3 bot:InputUser = bots.AccessSettings; +bots.editAccessSettings#31813cd8 flags:# restricted:flags.0?true bot:InputUser add_users:flags.1?Vector = Bool; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2970,7 +2975,7 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -aicompose.createTone#70f76f8 flags:# display_author:flags.0?true emoji_id:flags.1?long title:string prompt:string = AiComposeTone; +aicompose.createTone#4aa83913 flags:# display_author:flags.0?true emoji_id:long title:string prompt:string = AiComposeTone; aicompose.updateTone#903bcf59 flags:# tone:InputAiComposeTone display_author:flags.0?Bool emoji_id:flags.1?long title:flags.2?string prompt:flags.3?string = AiComposeTone; aicompose.saveTone#1782cbb1 tone:InputAiComposeTone unsave:Bool = Bool; aicompose.deleteTone#dd39316a tone:InputAiComposeTone = Bool; diff --git a/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.cpp b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.cpp new file mode 100644 index 0000000000..dd24b43d56 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.cpp @@ -0,0 +1,57 @@ +/* +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/custom_emoji_toast_icon.h" + +#include "data/stickers/data_custom_emoji.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "ui/text/text_custom_emoji.h" +#include "ui/rp_widget.h" + +#include "styles/style_widgets.h" + +#include + +namespace Ui { + +object_ptr MakeCustomEmojiToastIcon( + not_null session, + DocumentId emojiId, + QSize size) { + auto result = object_ptr((QWidget*)nullptr); + const auto raw = result.data(); + raw->resize(size); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + + struct State { + std::unique_ptr emoji; + }; + const auto state = raw->lifetime().make_state(); + state->emoji = session->data().customEmojiManager().create( + emojiId, + [=] { raw->update(); }, + Data::CustomEmojiManager::SizeTag::Large); + + raw->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(raw); + const auto esize = Emoji::GetCustomSizeLarge(); + const auto position = QPoint( + (raw->width() - esize) / 2, + (raw->height() - esize) / 2); + state->emoji->paint(p, { + .textColor = st::toastFg->c, + .now = crl::now(), + .position = position, + }); + }, raw->lifetime()); + + return result; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.h b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.h new file mode 100644 index 0000000000..844f1937e3 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.h @@ -0,0 +1,27 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Ui { + +[[nodiscard]] object_ptr MakeCustomEmojiToastIcon( + not_null session, + DocumentId emojiId, + QSize size); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/warning_tooltip.cpp b/Telegram/SourceFiles/ui/controls/warning_tooltip.cpp new file mode 100644 index 0000000000..0e98199f70 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/warning_tooltip.cpp @@ -0,0 +1,112 @@ +/* +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/warning_tooltip.h" + +#include "base/timer_rpl.h" +#include "ui/effects/animation_value.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" +#include "ui/ui_utility.h" + +#include "styles/style_layers.h" +#include "styles/style_widgets.h" + +namespace Ui { +namespace { + +constexpr auto kDefaultWarningTooltipDuration = crl::time(2000); + +} // namespace + +WarningTooltip::WarningTooltip() = default; + +WarningTooltip::~WarningTooltip() { + hide(anim::type::instant); +} + +void WarningTooltip::show(Args &&args) { + const auto duration = args.duration + ? args.duration + : kDefaultWarningTooltipDuration; + const auto maxWidth = args.maxWidth + ? args.maxWidth + : st::boxWideWidth; + const auto &tooltipSt = args.st + ? *args.st + : st::defaultImportantTooltip; + const auto &labelSt = args.labelSt + ? *args.labelSt + : st::defaultImportantTooltipLabel; + + const auto parent = args.parent.get(); + const auto target = args.target; + const auto side = args.side; + auto countPosition = std::move(args.countPosition); + const auto tooltip = CreateChild( + parent, + MakeNiceTooltipLabel( + parent, + std::move(args.text), + maxWidth, + labelSt), + tooltipSt); + tooltip->toggleFast(false); + + const auto update = [=] { + tooltip->pointAt( + MapFrom(parent, target, target->rect()), + side, + countPosition); + }; + parent->widthValue( + ) | rpl::on_next(update, tooltip->lifetime()); + update(); + tooltip->toggleAnimated(true); + + tooltip->shownValue( + ) | rpl::filter( + !rpl::mappers::_1 + ) | rpl::on_next([=] { + crl::on_main(tooltip, [=] { + if (tooltip->isHidden()) { + delete tooltip; + } + }); + }, tooltip->lifetime()); + + base::timer_once( + duration + ) | rpl::on_next([=] { + tooltip->toggleAnimated(false); + }, tooltip->lifetime()); + + if (_current && _current->tooltip) { + _current->tooltip->toggleAnimated(false); + } + auto entry = std::make_unique(); + entry->tooltip = tooltip; + _current = std::move(entry); +} + +void WarningTooltip::hide(anim::type animated) { + if (!_current) { + return; + } + const auto raw = _current->tooltip.data(); + _current.reset(); + if (!raw) { + return; + } + if (animated == anim::type::instant) { + delete raw; + } else { + raw->toggleAnimated(false); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/warning_tooltip.h b/Telegram/SourceFiles/ui/controls/warning_tooltip.h new file mode 100644 index 0000000000..1787dcd9ff --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/warning_tooltip.h @@ -0,0 +1,58 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" +#include "ui/rect_part.h" +#include "ui/text/text_entity.h" + +#include + +namespace style { +struct ImportantTooltip; +struct FlatLabel; +} // namespace style + +namespace anim { +enum class type : uchar; +} // namespace anim + +namespace Ui { + +class ImportantTooltip; +class RpWidget; + +class WarningTooltip final { +public: + struct Args { + not_null parent; + not_null target; + rpl::producer text; + RectParts side = RectPart::Top; + int maxWidth = 0; + Fn countPosition; + crl::time duration = 0; + const style::ImportantTooltip *st = nullptr; + const style::FlatLabel *labelSt = nullptr; + }; + + WarningTooltip(); + ~WarningTooltip(); + + void show(Args &&args); + void hide(anim::type animated); + +private: + struct Entry { + QPointer tooltip; + }; + std::unique_ptr _current; + +}; + +} // namespace Ui From 788411ac14434215f195a5d4f1e0f5f8aeebb90b Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 17 Apr 2026 16:03:26 +0700 Subject: [PATCH 095/154] Delete tone from Edit, restore scroll. --- Telegram/Resources/langs/lang.strings | 10 +- Telegram/SourceFiles/boxes/boxes.style | 13 +- Telegram/SourceFiles/boxes/compose_ai_box.cpp | 192 +++++++++--------- .../SourceFiles/boxes/create_ai_tone_box.cpp | 65 +++++- .../SourceFiles/boxes/create_ai_tone_box.h | 7 + .../ui/controls/labeled_emoji_tabs.cpp | 17 ++ .../ui/controls/labeled_emoji_tabs.h | 4 + 7 files changed, 208 insertions(+), 100 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 67ecbc536e..9ac4bbfb8e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7929,14 +7929,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_author" = "Add a link to my account"; "lng_ai_compose_tone_name_placeholder" = "Style Name (for example, \"Pirate\")"; "lng_ai_compose_tone_prompt_placeholder" = "Instructions (for example \"write in bold, nautical tone, light slang (aye, matey), vivid sea imagery, playful swagger, rhythmic phrasing, and adventurous mood\")"; -"lng_ai_compose_tone_edit" = "Edit"; -"lng_ai_compose_tone_share" = "Share"; -"lng_ai_compose_tone_delete" = "Delete"; -"lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style?"; +"lng_ai_compose_tone_edit" = "Edit Style"; +"lng_ai_compose_tone_share" = "Share Style"; +"lng_ai_compose_tone_delete" = "Delete Style"; +"lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style? It will be removed for everyone who installed it."; "lng_ai_compose_tone_save_sure" = "Do you want to save the style {title}?"; "lng_ai_compose_tone_saved" = "Style saved."; "lng_ai_compose_tone_link_copied" = "Style link copied."; -"lng_ai_compose_author" = "This style was created by {user}"; +"lng_ai_compose_author" = "Style by {user}"; "lng_ai_compose_tone_warn_icon" = "Please choose an icon."; "lng_ai_compose_tone_warn_name" = "Please choose a name."; "lng_ai_compose_tone_warn_prompt" = "Please enter instructions."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index f69bf8b671..ae550bb2e9 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1288,6 +1288,17 @@ aiTonePlaceholderLabel: FlatLabel(defaultFlatLabel) { } } aiToneAuthorCheckboxMargin: margins(0px, 12px, 0px, 8px); +aiToneDeleteButton: RoundButton(defaultActiveButton) { + textFg: attentionButtonFg; + textFgOver: attentionButtonFg; + textBg: boxBg; + textBgOver: boxBg; + height: 42px; + textTop: 12px; + style: semiboldTextStyle; + ripple: defaultRippleAnimation; +} +aiToneDeleteButtonMargin: margins(16px, 8px, 16px, 0px); aiComposeToneToastIconSize: size(32px, 32px); aiComposeToneToastIconPadding: margins(12px, 6px, 12px, 6px); @@ -1336,7 +1347,7 @@ aiComposeAuthorLabel: FlatLabel(defaultFlatLabel) { linkUnderline: kLinkUnderlineActive; } } -aiComposeAuthorLabelTop: 4px; +aiComposeAuthorLabelTop: 8px; aiComposeBoxButton: RoundButton(defaultActiveButton) { height: 42px; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index ef37a8f1fc..dbec0370b3 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -29,7 +29,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/platform/platform_language.h" #include "ui/boxes/about_cocoon_box.h" #include "ui/boxes/choose_language_box.h" -#include "ui/boxes/confirm_box.h" #include "ui/chat/chat_style.h" #include "ui/controls/labeled_emoji_tabs.h" #include "ui/controls/send_button.h" @@ -48,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" +#include "ui/widgets/menu/menu_action.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/tooltip.h" #include "styles/style_basic.h" @@ -337,7 +337,6 @@ public: void setEmojifyChecked(bool checked); void setState(CardState state); void setResultText(TextWithEntities text); - void setAuthorId(UserId authorId); void setShow(std::shared_ptr show); protected: @@ -349,7 +348,6 @@ private: void updateOriginalToggleIcon(); const Ui::Text::MarkedContext _context; - const not_null _session; const TextWithEntities _original; const not_null _originalTitle; const not_null _originalBody; @@ -357,7 +355,6 @@ private: const not_null _resultTitle; const not_null _resultBody; const not_null _copy; - const not_null _authorLabel; const not_null _emojify; Fn _resized; Fn _chooseCallback; @@ -370,7 +367,6 @@ private: bool _dividerVisible = false; int _dividerTop = 0; CardState _state = CardState::Waiting; - UserId _authorId = UserId(0); Ui::SkeletonAnimation _skeleton; std::array _diffColors; @@ -416,6 +412,7 @@ private: void resetState(CardState state); void applyResult(Api::ComposeWithAi::Result &&result); void showError(const QString &error = {}); + void setAuthorId(UserId authorId); void notifyLoadingChanged(); void notifyReadyChanged(); [[nodiscard]] QString currentTranslateStyle() const; @@ -433,6 +430,7 @@ private: QPointer _styles; QPointer> _stylesWrap; const not_null _preview; + const not_null _authorLabel; Fn _readyChanged; Fn _loadingChanged; Fn _premiumFlood; @@ -441,6 +439,7 @@ private: ComposeAiMode _mode = ComposeAiMode::Style; int _styleIndex = -1; int _translateStyleIndex = 0; + UserId _authorId = UserId(0); bool _emojify = false; CardState _state = CardState::Waiting; mtpRequestId _requestId = 0; @@ -605,7 +604,6 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( std::shared_ptr chatStyle) : RpWidget(parent) , _context(Core::TextContext({ .session = session })) -, _session(session) , _original(std::move(original)) , _originalTitle(Ui::CreateChild( this, @@ -625,9 +623,6 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( , _copy(Ui::CreateChild( this, st::aiComposeCopyButton)) -, _authorLabel(Ui::CreateChild( - this, - st::aiComposeAuthorLabel)) , _emojify( Ui::CreateChild( this, @@ -654,7 +649,6 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( }; watchHeight(_originalBody); watchHeight(_resultBody); - watchHeight(_authorLabel); _diffColors[0] = { &st::boxTextFgGood->p, &st::boxTextFgGood->p }; _diffColors[1] = { &st::attentionButtonFg->p, &st::attentionButtonFg->p }; _resultBody->setColors(_diffColors); @@ -680,7 +674,6 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( setResultTitle(tr::lng_ai_compose_result(tr::now, tr::marked)); _resultBody->setMarkedText(_original, _context); _copy->setVisible(false); - _authorLabel->setVisible(false); updateOriginalToggleIcon(); if (chatStyle) { const auto style = chatStyle; @@ -759,7 +752,6 @@ void ComposeAiPreviewCard::setState(CardState state) { case CardState::Failed: _resultBody->setMarkedText(_original, _context); _copy->setVisible(false); - _authorLabel->setVisible(false); if (wasLoading) { _skeleton.stop(); } @@ -767,12 +759,10 @@ void ComposeAiPreviewCard::setState(CardState state) { case CardState::Loading: _resultBody->setMarkedText(_original, _context); _copy->setVisible(false); - _authorLabel->setVisible(false); _skeleton.start(); break; case CardState::Ready: _copy->setVisible(true); - _authorLabel->setVisible(_authorId != 0); if (wasLoading) { _skeleton.stop(); } @@ -786,37 +776,6 @@ void ComposeAiPreviewCard::setResultText(TextWithEntities text) { refreshGeometry(); } -void ComposeAiPreviewCard::setAuthorId(UserId authorId) { - if (_authorId == authorId) { - return; - } - _authorId = authorId; - if (const auto user = _session->data().userLoaded(authorId)) { - const auto name = user->shortName(); - auto mention = tr::marked(name); - mention.entities.push_back(EntityInText( - EntityType::MentionName, - 0, - name.size(), - TextUtilities::MentionNameDataFromFields({ - .selfId = _session->userId().bare, - .userId = authorId.bare, - .accessHash = user->accessHash(), - }))); - _authorLabel->setMarkedText( - tr::lng_ai_compose_author( - tr::now, - lt_user, - std::move(mention), - tr::marked), - _context); - } else { - _authorLabel->setMarkedText({}); - _authorId = UserId(0); - } - refreshGeometry(); -} - void ComposeAiPreviewCard::setShow(std::shared_ptr show) { const auto setupFilter = [&](not_null label) { label->setClickHandlerFilter([=]( @@ -836,7 +795,6 @@ void ComposeAiPreviewCard::setShow(std::shared_ptr show) { }; setupFilter(_originalBody); setupFilter(_resultBody); - setupFilter(_authorLabel); } int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { @@ -930,11 +888,8 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { const auto lineHeight = _resultBody->st().style.lineHeight ? _resultBody->st().style.lineHeight : _resultBody->st().style.font->height; - const auto authorVisible = !_authorLabel->isHidden(); - if (!_copy->isHidden() && !authorVisible) { - _resultBody->setSkipBlock( - _copy->width(), - lineHeight); + if (!_copy->isHidden()) { + _resultBody->setSkipBlock(_copy->width(), lineHeight); } else { _resultBody->setSkipBlock(0, 0); } @@ -945,32 +900,13 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { contentWidth, _resultBody->height(), newWidth); - if (authorVisible) { - y += _resultBody->height() + st::aiComposeAuthorLabelTop; - const auto authorWidth = contentWidth - - _copy->width() - - st::aiComposeCardControlSkip; - _authorLabel->resizeToWidth(authorWidth); - _authorLabel->setGeometryToLeft( - padding.left(), - y, - authorWidth, - _authorLabel->height(), - newWidth); + if (!_copy->isHidden()) { _copy->moveToRight( padding.right(), - y + (_authorLabel->height() - _copy->height()) / 2, + y + _resultBody->height() - lineHeight, newWidth); - y += std::max(_authorLabel->height(), _copy->height()); - } else { - if (!_copy->isHidden()) { - _copy->moveToRight( - padding.right(), - y + _resultBody->height() - lineHeight, - newWidth); - } - y += _resultBody->height(); } + y += _resultBody->height(); return y + padding.bottom(); } @@ -1030,7 +966,10 @@ ComposeAiContent::ComposeAiContent( this, _session, _original, - args.chatStyle)) { + args.chatStyle)) +, _authorLabel(Ui::CreateChild( + this, + st::aiComposeAuthorLabel)) { _preview->setResizeCallback([=] { refreshLayout(); }); _preview->setChooseCallback([=] { chooseLanguage(); }); _preview->setCopyCallback([=] { copyResult(); }); @@ -1041,6 +980,26 @@ ComposeAiContent::ComposeAiContent( } }); _preview->setShow(_box->uiShow()); + _authorLabel->setVisible(false); + _authorLabel->heightValue( + ) | rpl::skip(1) | rpl::on_next([=] { + refreshLayout(); + }, lifetime()); + const auto show = _box->uiShow(); + _authorLabel->setClickHandlerFilter([=]( + const ClickHandlerPtr &handler, + Qt::MouseButton button) { + if (dynamic_cast(handler.get())) { + ActivateClickHandler(_authorLabel, handler, ClickContext{ + .button = button, + .other = QVariant::fromValue(ClickHandlerContext{ + .show = show, + }) + }); + return false; + } + return true; + }); } ComposeAiContent::~ComposeAiContent() { @@ -1150,6 +1109,7 @@ void ComposeAiContent::selectToneById(uint64 id) { updateTitles(); if (_styles) { _styles->setActive(_styleIndex); + _styles->scrollToActive(); } if (_mode == ComposeAiMode::Style) { request(); @@ -1171,7 +1131,16 @@ void ComposeAiContent::start() { int ComposeAiContent::resizeGetHeight(int newWidth) { _preview->resizeToWidth(newWidth); _preview->moveToLeft(0, 0, newWidth); - return _preview->height(); + auto y = _preview->height(); + if (!_authorLabel->isHidden()) { + _authorLabel->resizeToWidth(newWidth); + _authorLabel->moveToLeft( + 0, + y + st::aiComposeAuthorLabelTop, + newWidth); + y += st::aiComposeAuthorLabelTop + _authorLabel->height(); + } + return y; } void ComposeAiContent::refreshLayout() { @@ -1257,6 +1226,7 @@ void ComposeAiContent::setMode(ComposeAiMode mode) { _mode = mode; _state = CardState::Waiting; _preview->setState(CardState::Waiting); + setAuthorId(UserId(0)); notifyLoadingChanged(); if (_modeChanged) { _modeChanged(_mode); @@ -1371,10 +1341,43 @@ void ComposeAiContent::request() { }); } +void ComposeAiContent::setAuthorId(UserId authorId) { + if (_authorId == authorId) { + return; + } + _authorId = authorId; + if (const auto user = _session->data().userLoaded(authorId)) { + const auto name = user->shortName(); + auto mention = tr::marked(name); + mention.entities.push_back(EntityInText( + EntityType::MentionName, + 0, + name.size(), + TextUtilities::MentionNameDataFromFields({ + .selfId = _session->userId().bare, + .userId = authorId.bare, + .accessHash = user->accessHash(), + }))); + _authorLabel->setMarkedText( + tr::lng_ai_compose_author( + tr::now, + lt_user, + std::move(mention), + tr::marked), + Core::TextContext({ .session = _session })); + _authorLabel->setVisible(true); + } else { + _authorLabel->setMarkedText({}); + _authorLabel->setVisible(false); + _authorId = UserId(0); + } + refreshLayout(); +} + void ComposeAiContent::resetState(CardState state) { _state = state; _result = {}; - _preview->setAuthorId(UserId(0)); + setAuthorId(UserId(0)); _preview->setState(state); notifyLoadingChanged(); updateTitles(); @@ -1398,9 +1401,9 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) { if (_mode == ComposeAiMode::Style && _styleIndex >= 0 && _styleIndex < int(_tones.size())) { - _preview->setAuthorId(_tones[_styleIndex].authorId); + setAuthorId(_tones[_styleIndex].authorId); } else { - _preview->setAuthorId(UserId(0)); + setAuthorId(UserId(0)); } } updateTitles(); @@ -1410,6 +1413,7 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) { void ComposeAiContent::showError(const QString &error) { _state = CardState::Failed; + setAuthorId(UserId(0)); _preview->setState(CardState::Failed); notifyLoadingChanged(); updateTitles(); @@ -1618,7 +1622,9 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { content->setModeTabs(tabs); const auto rebuildStylesWrap = [=] { + auto savedScroll = -1; if (const auto old = stylesWrapHolder->data()) { + savedScroll = old->entity()->scrollLeft(); delete old; } if (const auto old = styleTooltipHolder->data()) { @@ -1671,22 +1677,26 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { tr::lng_ai_compose_tone_link_copied(tr::now)); }, &st::menuIconShare); - (*contextMenu)->addAction( - tr::lng_ai_compose_tone_delete(tr::now), - [=] { - box->uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_ai_compose_tone_delete_sure(), - .confirmed = [=](Fn &&close) { - close(); - session->data().aiComposeTones().remove(toneCopy); - }, - .confirmText = tr::lng_ai_compose_tone_delete(), - })); - }, - &st::menuIconDelete); + (*contextMenu)->addAction(base::make_unique_q( + (*contextMenu)->menu(), + st::menuWithIconsAttention, + Ui::Menu::CreateAction( + (*contextMenu)->menu().get(), + tr::lng_ai_compose_tone_delete(tr::now), + [=] { + ConfirmDeleteAiTone( + box->uiShow(), + session, + toneCopy); + }), + &st::menuIconDeleteAttention, + &st::menuIconDeleteAttention)); (*contextMenu)->popup(globalPos); }); content->setStyleTabs(ptr); + if (savedScroll >= 0) { + ptr->entity()->setScrollLeft(savedScroll); + } auto handle = SetupStyleTooltip( box, pinnedToTop, diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index d0d6b0ce79..ccf570f645 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/abstract_button.h" +#include "ui/boxes/confirm_box.h" #include "ui/controls/custom_emoji_toast_icon.h" #include "ui/controls/warning_tooltip.h" #include "ui/effects/animations.h" @@ -29,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" @@ -309,7 +311,8 @@ void SetupToneBox( bool initialDisplayAuthor, rpl::producer title, rpl::producer submitLabel, - Fn submit) { + Fn submit, + Fn requestDelete = nullptr) { box->setStyle(st::aiComposeBox); box->setNoContentMargin(true); box->setWidth(st::boxWideWidth); @@ -458,18 +461,43 @@ void SetupToneBox( st::aiToneAuthorCheckboxMargin, style::al_top); + const auto deleteButton = requestDelete + ? box->addRow( + object_ptr( + box, + tr::lng_ai_compose_tone_delete(), + st::aiToneDeleteButton), + st::aiToneDeleteButtonMargin) + : nullptr; + if (deleteButton) { + deleteButton->setFullRadius(true); + deleteButton->setClickedCallback(std::move(requestDelete)); + box->widthValue( + ) | rpl::on_next([=](int width) { + const auto &margin = st::aiToneDeleteButtonMargin; + deleteButton->setFullWidth( + width - margin.left() - margin.right()); + }, deleteButton->lifetime()); + } + rpl::combine( prompt->topValue(), promptDecor->placeholder->heightValue(), box->getDelegate()->contentHeightMaxValue() ) | rpl::on_next([=](int top, int phHeight, int contentHeight) { const auto pad = st::aiToneFieldPadding; + const auto deleteBlock = deleteButton + ? (deleteButton->heightNoMargins() + + st::aiToneDeleteButtonMargin.top() + + st::aiToneDeleteButtonMargin.bottom()) + : 0; prompt->setMaxHeight(contentHeight - top - st::aiToneFieldsMargin.bottom() - authorCheckbox->heightNoMargins() - st::aiToneAuthorCheckboxMargin.top() - - st::aiToneAuthorCheckboxMargin.bottom()); + - st::aiToneAuthorCheckboxMargin.bottom() + - deleteBlock); prompt->setMinHeight(phHeight + pad.top() + pad.bottom()); }, prompt->lifetime()); @@ -552,7 +580,8 @@ void CreateAiToneBox( saved(tone); } }); - }); + }, + nullptr); } void EditAiToneBox( @@ -592,5 +621,35 @@ void EditAiToneBox( saved(updated); } }); + }, + [=] { + auto toneCopy = Data::AiComposeTone(); + toneCopy.id = toneId; + toneCopy.accessHash = toneAccessHash; + ConfirmDeleteAiTone( + box->uiShow(), + session, + toneCopy, + [=] { box->closeBox(); }); }); } + +void ConfirmDeleteAiTone( + std::shared_ptr show, + not_null session, + const Data::AiComposeTone &tone, + Fn done) { + show->show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_delete_sure(), + .confirmed = [=](Fn &&close) { + close(); + session->data().aiComposeTones().remove(tone); + if (done) { + done(); + } + }, + .confirmText = tr::lng_box_delete(), + .confirmStyle = &st::attentionBoxButton, + .title = tr::lng_ai_compose_tone_delete(), + })); +} diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h index 01730aa33e..a6516a22fa 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.h +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -17,6 +17,7 @@ class Session; namespace Ui { class GenericBox; +class Show; } // namespace Ui void CreateAiToneBox( @@ -29,3 +30,9 @@ void EditAiToneBox( not_null session, const Data::AiComposeTone &tone, Fn saved = nullptr); + +void ConfirmDeleteAiTone( + std::shared_ptr show, + not_null session, + const Data::AiComposeTone &tone, + Fn done = nullptr); diff --git a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp index ef086baa37..933697fd1d 100644 --- a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp +++ b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp @@ -586,6 +586,7 @@ void LabeledEmojiScrollTabs::scrollToActive() { const auto index = _inner->_active; if (index < 0 || index >= int(_inner->_buttons.size())) { _scrollToActivePending = false; + _pendingScrollLeft.reset(); return; } const auto button = _inner->_buttons[index]; @@ -594,9 +595,23 @@ void LabeledEmojiScrollTabs::scrollToActive() { return; } _scrollToActivePending = false; + _pendingScrollLeft.reset(); scrollToButton(button->x(), button->x() + button->width(), false); } +int LabeledEmojiScrollTabs::scrollLeft() const { + return _scroll->scrollLeft(); +} + +void LabeledEmojiScrollTabs::setScrollLeft(int value) { + if (_scroll->width() <= 0) { + _pendingScrollLeft = value; + return; + } + _pendingScrollLeft.reset(); + _scroll->scrollToX(std::max(0, value)); +} + QString LabeledEmojiScrollTabs::currentId() const { return _inner->currentId(); } @@ -634,6 +649,8 @@ int LabeledEmojiScrollTabs::resizeGetHeight(int newWidth) { } if (_scrollToActivePending) { scrollToActive(); + } else if (_pendingScrollLeft) { + setScrollLeft(*_pendingScrollLeft); } updateFades(); diff --git a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h index 08300f5d08..dbff4f5bdc 100644 --- a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h +++ b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_custom_emoji.h" #include +#include #include namespace Ui { @@ -75,6 +76,8 @@ public: void setActive(int index); void setPaintOuterCorners(bool paint); void scrollToActive(); + [[nodiscard]] int scrollLeft() const; + void setScrollLeft(int value); [[nodiscard]] QString currentId() const; [[nodiscard]] int buttonCount() const; [[nodiscard]] rpl::producer requestShown() const; @@ -98,6 +101,7 @@ private: std::unique_ptr _dragScroll; bool _paintOuterCorners = true; bool _scrollToActivePending = false; + std::optional _pendingScrollLeft; }; From bb64902e9a3bcf5b8360f59438728881755d59f8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 20 Apr 2026 14:23:39 +0700 Subject: [PATCH 096/154] Update API scheme on layer 225. --- Telegram/SourceFiles/mtproto/scheme/api.tl | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 5334c9166d..8cab9f6c22 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -2150,14 +2150,6 @@ bots.requestedButton#f13bbcd7 webapp_req_id:string = bots.RequestedButton; messages.composedMessageWithAI#90d7adfa flags:# result_text:TextWithEntities diff_text:flags.0?TextWithEntities = messages.ComposedMessageWithAI; -channels.found#3128c4bc flags:# results:Vector chats:Vector users:Vector next_offset:flags.0?string = channels.Found; - -personalChannel#19bc407d user_id:long channel_id:long = PersonalChannel; - -channels.personalChannels#d69ae84d channels:Vector chats:Vector users:Vector = channels.PersonalChannels; - -channelTopic#93a5df73 id:int title:string = ChannelTopic; - stats.pollStats#2999beed votes_graph:StatsGraph = stats.PollStats; inputAiComposeToneDefault#1fe9a9bf tone:string = InputAiComposeTone; @@ -2732,9 +2724,6 @@ channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Upda channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool; -channels.search#27d79557 flags:# q:flags.0?string topic_id:flags.1?int offset:string = channels.Found; -channels.getContactPersonalChannels#509b3c66 = channels.PersonalChannels; -channels.getTopics#7ab18dcc lang_code:string = Vector; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; From 497b2a45bc81c6eef030931b75a4251b13ffd95e Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 17 Apr 2026 21:19:15 +0700 Subject: [PATCH 097/154] Make nice custom style tone preview and links. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 13 +- Telegram/SourceFiles/boxes/boxes.style | 51 ++- Telegram/SourceFiles/boxes/compose_ai_box.cpp | 17 +- .../SourceFiles/boxes/create_ai_tone_box.cpp | 46 ++- .../SourceFiles/boxes/create_ai_tone_box.h | 8 + .../SourceFiles/boxes/preview_ai_tone_box.cpp | 336 ++++++++++++++++++ .../SourceFiles/boxes/preview_ai_tone_box.h | 25 ++ .../SourceFiles/core/local_url_handlers.cpp | 26 +- .../data/data_ai_compose_tones.cpp | 35 +- .../SourceFiles/data/data_ai_compose_tones.h | 11 + Telegram/SourceFiles/data/data_session.cpp | 20 ++ Telegram/SourceFiles/data/data_session.h | 1 + Telegram/SourceFiles/data/data_web_page.cpp | 5 + Telegram/SourceFiles/data/data_web_page.h | 4 + .../view/media/history_view_web_page.cpp | 58 ++- .../view/media/history_view_web_page.h | 11 +- 17 files changed, 615 insertions(+), 54 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp create mode 100644 Telegram/SourceFiles/boxes/preview_ai_tone_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 43684ca368..1167fbcdcc 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -388,6 +388,8 @@ PRIVATE boxes/compose_ai_box.h boxes/create_ai_tone_box.cpp boxes/create_ai_tone_box.h + boxes/preview_ai_tone_box.cpp + boxes/preview_ai_tone_box.h boxes/translate_box.cpp boxes/translate_box.h boxes/url_auth_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9ac4bbfb8e..4a5648fd86 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7281,6 +7281,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_view_button_iv" = "Instant View"; "lng_view_button_stickerset" = "View stickers"; "lng_view_button_emojipack" = "View emoji"; +"lng_view_button_style" = "View Style"; "lng_view_button_collectible" = "View collectible"; "lng_view_button_call" = "Join call"; "lng_view_button_storyalbum" = "View Album"; @@ -7906,6 +7907,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tab_fix" = "Fix"; "lng_ai_compose_original" = "Original"; "lng_ai_compose_result" = "Result"; +"lng_ai_compose_before" = "Before"; +"lng_ai_compose_after" = "After"; "lng_ai_compose_to_language" = "To {language}"; "lng_ai_compose_name_style" = "{name} ({style})"; "lng_ai_compose_style_neutral" = "Neutral"; @@ -7933,8 +7936,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_share" = "Share Style"; "lng_ai_compose_tone_delete" = "Delete Style"; "lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style? It will be removed for everyone who installed it."; -"lng_ai_compose_tone_save_sure" = "Do you want to save the style {title}?"; -"lng_ai_compose_tone_saved" = "Style saved."; "lng_ai_compose_tone_link_copied" = "Style link copied."; "lng_ai_compose_author" = "Style by {user}"; "lng_ai_compose_tone_warn_icon" = "Please choose an icon."; @@ -7943,6 +7944,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_created" = "{title} Style Created!"; "lng_ai_compose_tone_updated" = "{title} Style Updated!"; "lng_ai_compose_tone_created_description" = "Right click the style to edit or share the link."; +"lng_ai_compose_tone_preview_about" = "Add this style to instantly rewrite your messages."; +"lng_ai_compose_tone_preview_add" = "Add Style"; +"lng_ai_compose_tone_preview_add_example" = "Another example"; +"lng_ai_compose_tone_preview_used_by#one" = "Used by {count} person."; +"lng_ai_compose_tone_preview_used_by#other" = "Used by {count} people."; +"lng_ai_compose_tone_preview_created_by" = "Created by {user}"; +"lng_ai_compose_tone_added" = "Style Added"; +"lng_ai_compose_tone_added_description" = "Tap \"AI\" → \"{name}\" when typing your next long message."; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index ae550bb2e9..1bb34d764d 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1302,10 +1302,58 @@ aiToneDeleteButtonMargin: margins(16px, 8px, 16px, 0px); aiComposeToneToastIconSize: size(32px, 32px); aiComposeToneToastIconPadding: margins(12px, 6px, 12px, 6px); +aiTonePreviewTitleLabel: FlatLabel(defaultFlatLabel) { + textFg: windowFg; + minWidth: 0px; + maxHeight: 28px; + align: align(top); + style: TextStyle(semiboldTextStyle) { + font: font(17px semibold); + } +} +aiTonePreviewTitleMargin: margins(16px, 6px, 16px, 0px); + +aiTonePreviewAboutLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 20px; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(14px); + } +} +aiTonePreviewAboutMargin: margins(28px, 6px, 28px, 6px); + +aiTonePreviewAttributionLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 0px; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineActive; + } +} +aiTonePreviewAttributionMargin: margins(16px, 10px, 16px, 8px); + +aiTonePreviewExampleCardBg: boxBg; +aiTonePreviewExampleCardRadius: 22px; +aiTonePreviewExampleCardPadding: margins(14px, 12px, 16px, 12px); +aiTonePreviewExampleCardSectionSkip: 24px; +aiTonePreviewExampleCardTitleSkip: 6px; +aiTonePreviewExampleCardMargin: margins(16px, 6px, 16px, 0px); + +aiTonePreviewAnotherExampleButton: RoundButton(defaultLightButton) { + width: -12px; + height: 26px; + radius: 8px; + textTop: 4px; + style: TextStyle(defaultTextStyle) { + font: font(12px semibold); + } +} + aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; aiComposeCardPadding: margins(12px, 16px, 16px, 16px); -aiComposeCardDivider: shadowFg; aiComposeCardSectionSkip: 12px; aiComposeCardControlSkip: 8px; aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) { @@ -1374,3 +1422,4 @@ aiComposeBoxInfoButton: IconButton(boxTitleClose) { iconOver: icon {{ "menu/info", boxTitleCloseFgOver }}; ripple: defaultRippleAnimation; } +aiComposeShadowOpacity: 0.3; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index dbec0370b3..88e378de60 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "boxes/create_ai_tone_box.h" #include "boxes/premium_preview_box.h" +#include "boxes/share_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_lottie.h" #include "core/application.h" @@ -60,8 +61,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#include - namespace HistoryView::Controls { namespace { @@ -922,7 +921,9 @@ void ComposeAiPreviewCard::paintEvent(QPaintEvent *e) { st::aiComposeCardRadius); if (_dividerVisible) { p.setBrush(Qt::NoBrush); - p.setPen(st::aiComposeCardDivider); + auto color = st::windowSubTextFg->c; + color.setAlphaF(st::aiComposeShadowOpacity); + p.setPen(color); p.drawLine( st::aiComposeCardPadding.left(), _dividerTop, @@ -1670,11 +1671,11 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { (*contextMenu)->addAction( tr::lng_ai_compose_tone_share(tr::now), [=] { - QGuiApplication::clipboard()->setText( - session->createInternalLinkFull( - "aistyle/" + toneCopy.slug)); - box->showToast( - tr::lng_ai_compose_tone_link_copied(tr::now)); + const auto url = session->createInternalLinkFull( + "addstyle/" + toneCopy.slug); + FastShareLink( + Main::MakeSessionShow(box->uiShow(), session), + url); }, &st::menuIconShare); (*contextMenu)->addAction(base::make_unique_q( diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index ccf570f645..522ef60cdb 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -157,7 +157,9 @@ void ChooseToneIconBox( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } -not_null AddIconPreview( +} // namespace + +not_null AddAiToneIconPreview( not_null container, not_null session, rpl::producer emojiIdValue, @@ -285,23 +287,29 @@ not_null AddIconPreview( } }, button->lifetime()); - button->setClickedCallback([=] { - const auto controller = ChatHelpers::ResolveWindowDefault()( - session); - if (!controller) { - return; - } - controller->uiShow()->showBox(Box( - ChooseToneIconBox, - controller, - crl::guard(button, [=](DocumentId id) { - emojiIdChosen(id); - }))); - }); + if (emojiIdChosen) { + button->setClickedCallback([=] { + const auto controller = ChatHelpers::ResolveWindowDefault()( + session); + if (!controller) { + return; + } + controller->uiShow()->showBox(Box( + ChooseToneIconBox, + controller, + crl::guard(button, [=](DocumentId id) { + emojiIdChosen(id); + }))); + }); + } else { + button->setAttribute(Qt::WA_TransparentForMouseEvents); + } return button; } +namespace { + void SetupToneBox( not_null box, not_null session, @@ -323,7 +331,7 @@ void SetupToneBox( const auto emojiId = container->lifetime().make_state< rpl::variable>(initialEmojiId); - const auto iconButton = AddIconPreview( + const auto iconButton = AddAiToneIconPreview( container, session, emojiId->value(), @@ -572,14 +580,14 @@ void CreateAiToneBox( prompt, emojiId, displayAuthor, - [=](Data::AiComposeTone tone) { + crl::guard(box, [=](Data::AiComposeTone tone) { const auto show = box->uiShow(); box->closeBox(); ShowToneToast(show, session, tone, true); if (saved) { saved(tone); } - }); + })); }, nullptr); } @@ -613,14 +621,14 @@ void EditAiToneBox( prompt, std::make_optional(emojiId), std::make_optional(displayAuthor), - [=](Data::AiComposeTone updated) { + crl::guard(box, [=](Data::AiComposeTone updated) { const auto show = box->uiShow(); box->closeBox(); ShowToneToast(show, session, updated, false); if (saved) { saved(updated); } - }); + })); }, [=] { auto toneCopy = Data::AiComposeTone(); diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h index a6516a22fa..d91daaf6a7 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.h +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -16,10 +16,18 @@ class Session; } // namespace Main namespace Ui { +class AbstractButton; class GenericBox; class Show; +class VerticalLayout; } // namespace Ui +not_null AddAiToneIconPreview( + not_null container, + not_null session, + rpl::producer emojiIdValue, + Fn emojiIdChosen = nullptr); + void CreateAiToneBox( not_null box, not_null session, diff --git a/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp new file mode 100644 index 0000000000..b73a4d2680 --- /dev/null +++ b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp @@ -0,0 +1,336 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/preview_ai_tone_box.h" + +#include "boxes/create_ai_tone_box.h" +#include "core/ui_integration.h" +#include "data/data_ai_compose_tones.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/controls/custom_emoji_toast_icon.h" +#include "ui/effects/skeleton_animation.h" +#include "ui/layers/generic_box.h" +#include "ui/layers/show.h" +#include "ui/painter.h" +#include "ui/text/text_entity.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" + +namespace { + +constexpr auto kToastDuration = crl::time(4000); + +class PreviewAiToneExampleCard final : public Ui::RpWidget { +public: + explicit PreviewAiToneExampleCard(QWidget *parent); + + void showExample(Data::AiComposeToneExample example); + void showSkeleton(bool shown); + [[nodiscard]] rpl::producer<> anotherExampleRequested() const; + +protected: + int resizeGetHeight(int newWidth) override; + void paintEvent(QPaintEvent *e) override; + +private: + const not_null _layout; + const not_null _beforeTitle; + const not_null _beforeBody; + const style::complex_color _shadowColor; + const not_null _shadow; + const not_null _afterTitle; + const not_null _afterBody; + const not_null _another; + Ui::SkeletonAnimation _beforeSkeleton; + Ui::SkeletonAnimation _afterSkeleton; + rpl::event_stream<> _anotherExampleRequested; + +}; + +PreviewAiToneExampleCard::PreviewAiToneExampleCard(QWidget *parent) +: RpWidget(parent) +, _layout(Ui::CreateChild(this)) +, _beforeTitle(_layout->add( + object_ptr( + _layout, + tr::lng_ai_compose_before(tr::now), + st::aiComposeCardTitle), + QMargins( + st::aiTonePreviewExampleCardPadding.left(), + st::aiTonePreviewExampleCardPadding.top(), + st::aiTonePreviewExampleCardPadding.right(), + 0))) +, _beforeBody(_layout->add( + object_ptr(_layout, st::aiComposeBodyLabel), + QMargins( + st::aiTonePreviewExampleCardPadding.left(), + st::aiTonePreviewExampleCardTitleSkip, + st::aiTonePreviewExampleCardPadding.right(), + 0))) +, _shadowColor([] { + auto color = st::windowSubTextFg->c; + color.setAlphaF(st::aiComposeShadowOpacity); + return color; +}) +, _shadow(_layout->add( + object_ptr(_layout, _shadowColor.color()), + QMargins( + st::aiTonePreviewExampleCardPadding.left(), + st::aiTonePreviewExampleCardSectionSkip / 2, + st::aiTonePreviewExampleCardPadding.right(), + 0))) +, _afterTitle(_layout->add( + object_ptr( + _layout, + tr::lng_ai_compose_after(tr::now), + st::aiComposeCardTitle), + QMargins( + st::aiTonePreviewExampleCardPadding.left(), + st::aiTonePreviewExampleCardSectionSkip / 2, + st::aiTonePreviewExampleCardPadding.right(), + 0))) +, _afterBody(_layout->add( + object_ptr(_layout, st::aiComposeBodyLabel), + QMargins( + st::aiTonePreviewExampleCardPadding.left(), + st::aiTonePreviewExampleCardTitleSkip, + st::aiTonePreviewExampleCardPadding.right(), + st::aiTonePreviewExampleCardPadding.bottom()))) +, _another(Ui::CreateChild( + this, + tr::lng_ai_compose_tone_preview_add_example(), + st::aiTonePreviewAnotherExampleButton)) +, _beforeSkeleton(_beforeBody) +, _afterSkeleton(_afterBody) { + _beforeBody->setSelectable(true); + _afterBody->setSelectable(true); + _another->raise(); + rpl::combine( + widthValue(), + _beforeTitle->geometryValue(), + _another->widthValue() + ) | rpl::on_next([=](int width, QRect titleGeometry, int) { + const auto right = st::aiTonePreviewExampleCardPadding.left(); + const auto &button = st::aiTonePreviewAnotherExampleButton; + const auto &title = st::aiComposeCardTitle; + const auto shift = title.style.font->ascent + - button.style.font->ascent + - button.textTop + - button.padding.top(); + _another->moveToRight( + right, + titleGeometry.top() + shift, + width); + }, lifetime()); + _another->setClickedCallback([=] { + _anotherExampleRequested.fire({}); + }); +} + +void PreviewAiToneExampleCard::showExample( + Data::AiComposeToneExample example) { + _beforeBody->setText(example.from); + _afterBody->setText(example.to); + _beforeSkeleton.stop(); + _afterSkeleton.stop(); + if (width() > 0) { + resizeToWidth(width()); + } +} + +void PreviewAiToneExampleCard::showSkeleton(bool shown) { + if (shown) { + _beforeSkeleton.start(); + _afterSkeleton.start(); + } else { + _beforeSkeleton.stop(); + _afterSkeleton.stop(); + } +} + +rpl::producer<> PreviewAiToneExampleCard::anotherExampleRequested() const { + return _anotherExampleRequested.events(); +} + +int PreviewAiToneExampleCard::resizeGetHeight(int newWidth) { + _layout->resizeToWidth(newWidth); + _layout->moveToLeft(0, 0, newWidth); + return _layout->heightNoMargins(); +} + +void PreviewAiToneExampleCard::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::aiTonePreviewExampleCardBg); + p.drawRoundedRect( + rect(), + st::aiTonePreviewExampleCardRadius, + st::aiTonePreviewExampleCardRadius); +} + +void ShowToneAddedToast( + std::shared_ptr show, + not_null session, + const Data::AiComposeTone &tone) { + const auto size = QSize( + st::aiComposeToneToastIconSize.width(), + st::aiComposeToneToastIconSize.height()); + show->showToast(Ui::Toast::Config{ + .title = tr::lng_ai_compose_tone_added(tr::now), + .text = tr::lng_ai_compose_tone_added_description( + tr::now, + lt_name, + tr::marked(tone.title), + tr::marked), + .iconContent = Ui::MakeCustomEmojiToastIcon( + session, + tone.emojiId, + size), + .iconPadding = st::aiComposeToneToastIconPadding, + .duration = kToastDuration, + }); +} + +} // namespace + +void PreviewAiToneBox( + not_null box, + not_null session, + Data::AiComposeTone tone) { + box->setStyle(st::aiComposeBox); + box->setNoContentMargin(true); + box->setWidth(st::boxWideWidth); + box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); }); + + const auto top = box->setPinnedToTopContent( + object_ptr(box)); + Ui::AddSkip(top, st::defaultVerticalListSkip * 4); + AddAiToneIconPreview(top, session, rpl::single(tone.emojiId), nullptr); + top->add( + object_ptr( + top, + rpl::single(tone.title), + st::aiTonePreviewTitleLabel), + st::aiTonePreviewTitleMargin, + style::al_top); + top->add( + object_ptr( + top, + tr::lng_ai_compose_tone_preview_about(), + st::aiTonePreviewAboutLabel), + st::aiTonePreviewAboutMargin, + style::al_top + )->setTryMakeSimilarLines(true); + + const auto body = box->verticalLayout(); + + struct State { + int examplesCount = 0; + bool requesting = false; + }; + const auto state = box->lifetime().make_state(); + state->examplesCount = tone.firstExample ? 1 : 0; + + const auto card = body->add( + object_ptr(body), + st::aiTonePreviewExampleCardMargin); + const auto loadAnother = [=] { + if (state->requesting) { + return; + } + state->requesting = true; + card->showSkeleton(true); + const auto num = state->examplesCount; + session->data().aiComposeTones().getToneExample( + tone, + num, + crl::guard(box, [=](Data::AiComposeToneExample example) { + state->requesting = false; + ++state->examplesCount; + card->showExample(std::move(example)); + }), + crl::guard(box, [=](const MTP::Error &) { + state->requesting = false; + card->showSkeleton(false); + box->showToast(tr::lng_ai_compose_error(tr::now)); + })); + }; + card->anotherExampleRequested( + ) | rpl::on_next(loadAnother, card->lifetime()); + + if (tone.firstExample) { + card->showExample(*tone.firstExample); + } else { + loadAnother(); + } + + const auto attribution = body->add( + object_ptr(body, st::aiTonePreviewAttributionLabel), + st::aiTonePreviewAttributionMargin, + style::al_top); + + auto text = tr::marked(); + if (tone.installsCount > 0) { + text = tr::lng_ai_compose_tone_preview_used_by( + tr::now, + lt_count, + tone.installsCount, + tr::marked); + } + if (const auto user = session->data().userLoaded(tone.authorId)) { + const auto name = user->shortName(); + auto mention = tr::marked(name); + mention.entities.push_back(EntityInText( + EntityType::MentionName, + 0, + name.size(), + TextUtilities::MentionNameDataFromFields({ + .selfId = session->userId().bare, + .userId = tone.authorId.bare, + .accessHash = user->accessHash(), + }))); + auto createdBy = tr::lng_ai_compose_tone_preview_created_by( + tr::now, + lt_user, + std::move(mention), + tr::marked); + if (!text.empty()) { + text.append(' ').append(std::move(createdBy)); + } else { + text = std::move(createdBy); + } + } + if (text.empty()) { + attribution->setVisible(false); + } else { + attribution->setMarkedText( + std::move(text), + Core::TextContext({ .session = session })); + } + + const auto add = box->addButton( + tr::lng_ai_compose_tone_preview_add(), + [=] { + session->data().aiComposeTones().save(tone, false); + const auto show = box->uiShow(); + box->closeBox(); + ShowToneAddedToast(show, session, tone); + }); + add->setFullRadius(true); +} diff --git a/Telegram/SourceFiles/boxes/preview_ai_tone_box.h b/Telegram/SourceFiles/boxes/preview_ai_tone_box.h new file mode 100644 index 0000000000..5291ff423b --- /dev/null +++ b/Telegram/SourceFiles/boxes/preview_ai_tone_box.h @@ -0,0 +1,25 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +struct AiComposeTone; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class GenericBox; +} // namespace Ui + +void PreviewAiToneBox( + not_null box, + not_null session, + Data::AiComposeTone tone); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 62c3e924fc..4df29cde34 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/gift_premium_box.h" #include "boxes/edit_privacy_box.h" #include "boxes/premium_preview_box.h" +#include "boxes/preview_ai_tone_box.h" #include "boxes/sticker_set_box.h" #include "boxes/star_gift_box.h" #include "boxes/language_box.h" @@ -311,21 +312,10 @@ bool ShowAiStyle( if (!strong) { return; } - strong->window().show(Ui::MakeConfirmBox({ - .text = tr::lng_ai_compose_tone_save_sure( - tr::now, - lt_title, - tone.title), - .confirmed = [=](Fn &&close) { - close(); - strong->session().data().aiComposeTones().save( - tone, - false); - strong->window().showToast( - tr::lng_ai_compose_tone_saved(tr::now)); - }, - .confirmText = tr::lng_ai_compose_tone_save(), - })); + strong->window().show(Box( + PreviewAiToneBox, + &strong->session(), + std::move(tone))); }, [=](const MTP::Error &error) { const auto strong = weak.get(); if (!strong) { @@ -1705,7 +1695,7 @@ const std::vector &LocalUrlHandlers() { ShowTheme }, { - u"^aistyle/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, + u"^addstyle/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, ShowAiStyle }, { @@ -1925,8 +1915,8 @@ QString TryConvertUrlToLocal(QString url) { return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2)); } else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://addtheme?slug="_q + url_encode(themeMatch->captured(1)); - } else if (const auto aiStyleMatch = regex_match(u"^aistyle/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { - return u"tg://aistyle?slug="_q + url_encode(aiStyleMatch->captured(1)); + } else if (const auto addStyleMatch = regex_match(u"^addstyle/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { + return u"tg://addstyle?slug="_q + url_encode(addStyleMatch->captured(1)); } else if (const auto languageMatch = regex_match(u"^setlanguage/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://setlanguage?lang="_q + url_encode(languageMatch->captured(1)); } else if (const auto shareUrlMatch = regex_match(u"^share/url/?\\?(.+)$"_q, query, matchOptions)) { diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp index 6b3b4dc616..d707d22b62 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -56,7 +56,7 @@ void AiComposeTones::parseTones(const QVector &list) { AiComposeTone AiComposeTones::parseTone( const MTPAiComposeTone &tone) const { return tone.match([&](const MTPDaiComposeTone &data) { - return AiComposeTone{ + auto result = AiComposeTone{ .id = data.vid().v, .accessHash = data.vaccess_hash().v, .slug = qs(data.vslug()), @@ -69,6 +69,15 @@ AiComposeTone AiComposeTones::parseTone( : UserId(0), .creator = data.is_creator(), }; + if (const auto example = data.vexample_english()) { + example->match([&](const MTPDaiComposeToneExample &d) { + result.firstExample = AiComposeToneExample{ + .from = qs(d.vfrom()), + .to = qs(d.vto()), + }; + }); + } + return result; }, [&](const MTPDaiComposeToneDefault &data) { return AiComposeTone{ .title = qs(data.vtitle()), @@ -239,6 +248,30 @@ void AiComposeTones::resolve( }).send(); } +void AiComposeTones::getToneExample( + const AiComposeTone &tone, + int num, + Fn done, + Fn fail) { + _session->api().request(MTPaicompose_GetToneExample( + toneToMTP(tone), + MTP_int(num) + )).done([=](const MTPAiComposeToneExample &result) { + result.match([&](const MTPDaiComposeToneExample &data) { + if (done) { + done(AiComposeToneExample{ + .from = qs(data.vfrom()), + .to = qs(data.vto()), + }); + } + }); + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } + }).send(); +} + void AiComposeTones::applyUpdate() { refresh(); } diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.h b/Telegram/SourceFiles/data/data_ai_compose_tones.h index c17e8f2738..d4045799b5 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.h +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.h @@ -19,6 +19,11 @@ class Error; namespace Data { +struct AiComposeToneExample { + QString from; + QString to; +}; + struct AiComposeTone { uint64 id = 0; uint64 accessHash = 0; @@ -31,6 +36,7 @@ struct AiComposeTone { bool creator = false; bool isDefault = false; QString defaultType; + std::optional firstExample; }; class AiComposeTones final { @@ -67,6 +73,11 @@ public: const QString &slug, Fn done, Fn fail = nullptr); + void getToneExample( + const AiComposeTone &tone, + int num, + Fn done, + Fn fail = nullptr); void applyUpdate(); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index ebd2db5b37..74efd0bd79 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -3890,6 +3890,7 @@ not_null Session::processWebpage( nullptr, nullptr, 0, + 0, QString(), false, false, @@ -3960,6 +3961,7 @@ not_null Session::webpage( std::move(stickerSet), std::move(uniqueGift), nullptr, + 0, duration, author, hasLargeMedia, @@ -4089,6 +4091,21 @@ void Session::webpageApplyFields( return nullptr; }; + const auto lookupComposeToneEmojiId = [&]() -> DocumentId { + if (const auto attributes = data.vattributes()) { + for (const auto &attribute : attributes->v) { + const auto result = attribute.match([&]( + const MTPDwebPageAttributeAiComposeTone &data) { + return DocumentId(data.vemoji_id().v); + }, [](const auto &) { return DocumentId(0); }); + if (result) { + return result; + } + } + } + return 0; + }; + auto story = (Data::Story*)nullptr; auto storyId = FullStoryId(); if (const auto attributes = data.vattributes()) { @@ -4190,6 +4207,7 @@ void Session::webpageApplyFields( lookupStickerSet(), lookupUniqueGift(), lookupAuction(), + lookupComposeToneEmojiId(), data.vduration().value_or_empty(), qs(data.vauthor().value_or_empty()), data.is_has_large_media(), @@ -4213,6 +4231,7 @@ void Session::webpageApplyFields( std::unique_ptr stickerSet, std::shared_ptr uniqueGift, std::unique_ptr auction, + DocumentId composeToneEmojiId, int duration, const QString &author, bool hasLargeMedia, @@ -4234,6 +4253,7 @@ void Session::webpageApplyFields( std::move(stickerSet), std::move(uniqueGift), std::move(auction), + composeToneEmojiId, duration, author, hasLargeMedia, diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index f41afac491..bcff8e4982 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -1075,6 +1075,7 @@ private: std::unique_ptr stickerSet, std::shared_ptr uniqueGift, std::unique_ptr auction, + DocumentId composeToneEmojiId, int duration, const QString &author, bool hasLargeMedia, diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index c96c256912..32a195d640 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -182,6 +182,8 @@ WebPageType ParseWebPageType( return WebPageType::Auction; } else if (type == u"telegram_newbot"_q) { return WebPageType::NewBot; + } else if (type == u"telegram_aicomposetone"_q) { + return WebPageType::ComposeAiTone; } else if (hasIV) { return WebPageType::ArticleWithIV; } else { @@ -238,6 +240,7 @@ bool WebPageData::applyChanges( std::unique_ptr newStickerSet, std::shared_ptr newUniqueGift, std::unique_ptr newAuction, + DocumentId newComposeToneEmojiId, int newDuration, const QString &newAuthor, bool newHasLargeMedia, @@ -301,6 +304,7 @@ bool WebPageData::applyChanges( && (!stickerSet == !newStickerSet) && (!uniqueGift == !newUniqueGift) && (!auction == !newAuction) + && composeToneEmojiId == newComposeToneEmojiId && duration == newDuration && author == resultAuthor && hasLargeMedia == (newHasLargeMedia ? 1 : 0) @@ -327,6 +331,7 @@ bool WebPageData::applyChanges( stickerSet = std::move(newStickerSet); uniqueGift = std::move(newUniqueGift); auction = std::move(newAuction); + composeToneEmojiId = newComposeToneEmojiId; duration = newDuration; author = resultAuthor; pendingTill = newPendingTill; diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index 4deca2435e..6baedccb2a 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -55,6 +55,8 @@ enum class WebPageType : uint8 { Auction, NewBot, + ComposeAiTone, + Article, ArticleWithIV, @@ -115,6 +117,7 @@ struct WebPageData { std::unique_ptr newStickerSet, std::shared_ptr newUniqueGift, std::unique_ptr newAuction, + DocumentId newComposeToneEmojiId, int newDuration, const QString &newAuthor, bool newHasLargeMedia, @@ -147,6 +150,7 @@ struct WebPageData { std::unique_ptr stickerSet; std::shared_ptr uniqueGift; std::unique_ptr auction; + DocumentId composeToneEmojiId = 0; int duration = 0; TimeId pendingTill = 0; uint32 version : 29 = 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 5950801174..4bd2c7d725 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -16,7 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "data/components/sponsored_messages.h" -#include "data/stickers/data_custom_emoji.h" #include "data/data_file_click_handler.h" #include "data/data_photo_media.h" #include "data/data_session.h" @@ -233,6 +232,8 @@ constexpr auto kSponsoredUserpicLines = 2; ? tr::lng_view_button_emojipack(tr::now) : (type == WebPageType::StickerSet) ? tr::lng_view_button_stickerset(tr::now) + : (type == WebPageType::ComposeAiTone) + ? tr::lng_view_button_style(tr::now) : (type == WebPageType::StoryAlbum) ? tr::lng_view_button_storyalbum(tr::now) : (type == WebPageType::GiftCollection) @@ -286,6 +287,7 @@ constexpr auto kSponsoredUserpicLines = 2; || (type == WebPageType::StickerSet) || (type == WebPageType::StoryAlbum) || (type == WebPageType::GiftCollection) + || (type == WebPageType::ComposeAiTone) || (type == WebPageType::Auction) || (type == WebPageType::NewBot); } @@ -379,11 +381,57 @@ void WebPage::setupAdditionalData() { view->setWebpagePart(); view->initSize(single); } + } else if (_data->type == WebPageType::ComposeAiTone + && _data->composeToneEmojiId) { + if (const auto existing = stickerSetData() + ; existing && !existing->views.empty()) { + } else { + _additionalData = std::make_unique( + StickerSetData()); + const auto raw = stickerSetData(); + const auto session = &_data->session(); + const auto box = UnitedLineHeight() * kStickerSetLines; + const auto id = _data->composeToneEmojiId; + auto &manager = session->data().customEmojiManager(); + const auto document = session->data().document(id).get(); + if (document->sticker()) { + auto view = std::make_unique( + _parent, + document, + true); + view->setWebpagePart(); + view->initSize(box); + raw->views.push_back(std::move(view)); + } else { + manager.resolve(id, this); + _composeToneListening = 1; + } + } } else if (_data->type == WebPageType::Factcheck) { _additionalData = std::make_unique(FactcheckData()); } } +void WebPage::customEmojiResolveDone(not_null document) { + if (!document->sticker()) { + return; + } + if (_data->composeToneEmojiId != document->id) { + return; + } + const auto raw = stickerSetData(); + if (!raw) { + return; + } + const auto box = UnitedLineHeight() * kStickerSetLines; + auto view = std::make_unique(_parent, document, true); + view->setWebpagePart(); + view->initSize(box); + raw->views.clear(); + raw->views.push_back(std::move(view)); + history()->owner().requestViewResize(_parent); +} + QSize WebPage::countOptimalSize() { if (_data->pendingTill || _data->failed) { return { 0, 0 }; @@ -1018,8 +1066,8 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { const auto viewsCount = stickerSet->views.size(); const auto box = _pixh; const auto topLeft = QPoint(inner.left() + paintw - box, tshift); - const auto side = std::ceil(std::sqrt(viewsCount)); - const auto single = box / side; + const auto side = int(std::ceil(std::sqrt(viewsCount))); + const auto single = side ? (box / side) : box; for (auto i = 0; i < side; i++) { for (auto j = 0; j < side; j++) { const auto index = i * side + j; @@ -1751,6 +1799,10 @@ int WebPage::bottomInfoPadding() const { WebPage::~WebPage() { history()->owner().unregisterWebPageView(_data, _parent); + if (_composeToneListening) { + _data->session().data().customEmojiManager().unregisterListener( + this); + } if (_photoMedia) { history()->owner().keepAlive(base::take(_photoMedia)); _parent->checkHeavyPart(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.h b/Telegram/SourceFiles/history/view/media/history_view_web_page.h index 63cca4e6c5..34c06f0a3a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.h +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/stickers/data_custom_emoji.h" #include "history/view/media/history_view_media.h" #include "ui/userpic_view.h" @@ -24,7 +25,9 @@ namespace HistoryView { class Sticker; -class WebPage : public Media { +class WebPage final + : public Media + , private Data::CustomEmojiManager::Listener { public: WebPage( not_null parent, @@ -181,6 +184,9 @@ private: void setupAdditionalData(); + void customEmojiResolveDone( + not_null document) override; + const style::QuoteStyle &_st; const not_null _data; const MediaWebPageFlags _flags; @@ -194,8 +200,9 @@ private: int _dataVersion = -1; int _siteNameLines = 0; int _descriptionLines = 0; - uint32 _titleLines : 31 = 0; + uint32 _titleLines : 30 = 0; uint32 _asArticle : 1 = 0; + uint32 _composeToneListening : 1 = 0; Ui::Text::String _siteName; Ui::Text::String _title; From 0925f69df0c66f139e94a21e3b136f339d3bcce0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 21 Apr 2026 14:07:21 +0700 Subject: [PATCH 098/154] Add nice loading spinner to tone preview. --- Telegram/Resources/icons/chat/refresh.svg | 7 + Telegram/SourceFiles/boxes/boxes.style | 4 + .../SourceFiles/boxes/preview_ai_tone_box.cpp | 180 +++++++++++++++++- 3 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 Telegram/Resources/icons/chat/refresh.svg diff --git a/Telegram/Resources/icons/chat/refresh.svg b/Telegram/Resources/icons/chat/refresh.svg new file mode 100644 index 0000000000..10e1e400ac --- /dev/null +++ b/Telegram/Resources/icons/chat/refresh.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 1bb34d764d..7b2fdad992 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1350,6 +1350,10 @@ aiTonePreviewAnotherExampleButton: RoundButton(defaultLightButton) { font: font(12px semibold); } } +aiTonePreviewAnotherExampleIcon: IconEmoji { + icon: icon {{ "chat/refresh-18x18", lightButtonFg }}; + padding: margins(0px, 1px, 4px, 0px); +} aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; diff --git a/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp index b73a4d2680..a5374c3a67 100644 --- a/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp @@ -15,10 +15,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/controls/custom_emoji_toast_icon.h" +#include "ui/effects/animation_value.h" +#include "ui/effects/animations.h" #include "ui/effects/skeleton_animation.h" #include "ui/layers/generic_box.h" #include "ui/layers/show.h" #include "ui/painter.h" +#include "ui/text/text_custom_emoji.h" #include "ui/text/text_entity.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -34,10 +37,151 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kToastDuration = crl::time(4000); +constexpr auto kSpinDuration = crl::time(600); +constexpr auto kWaitDuration = crl::time(1000); + +class RefreshSpinEmoji final : public Ui::Text::CustomEmoji { +public: + RefreshSpinEmoji( + std::shared_ptr> loading, + Fn repaint); + + int width() override; + QString entityData() override; + void paint(QPainter &p, const Context &context) override; + void unload() override; + bool ready() override; + bool readyInDefaultState() override; + +private: + enum class Phase : uchar { Idle, Spinning, Waiting }; + + void handleLoading(bool now); + void tick(crl::time now); + [[nodiscard]] float64 angleDegrees(crl::time now) const; + + rpl::variable _loading; + const Fn _repaint; + Ui::Animations::Basic _animation; + crl::time _phaseStarted = 0; + Phase _phase = Phase::Idle; + rpl::lifetime _lifetime; + +}; + +RefreshSpinEmoji::RefreshSpinEmoji( + std::shared_ptr> loading, + Fn repaint) +: _loading(loading->value()) +, _repaint(std::move(repaint)) +, _animation([=](crl::time now) { tick(now); }) { + _loading.value( + ) | rpl::on_next([=](bool value) { + handleLoading(value); + }, _lifetime); +} + +int RefreshSpinEmoji::width() { + const auto &e = st::aiTonePreviewAnotherExampleIcon; + return e.padding.left() + e.icon.width() + e.padding.right(); +} + +QString RefreshSpinEmoji::entityData() { + return u"ai-tone-refresh"_q; +} + +float64 RefreshSpinEmoji::angleDegrees(crl::time now) const { + if (_phase != Phase::Spinning) { + return 0.; + } + const auto elapsed = now - _phaseStarted; + const auto dt = std::clamp( + elapsed / float64(kSpinDuration), + 0., + 1.); + return anim::easeOutBack(360., dt); +} + +void RefreshSpinEmoji::paint(QPainter &p, const Context &context) { + const auto &e = st::aiTonePreviewAnotherExampleIcon; + const auto size = e.icon.size(); + const auto pos = context.position + + QPoint(e.padding.left(), e.padding.top()); + const auto angle = angleDegrees(context.now); + auto hq = PainterHighQualityEnabler(p); + if (angle != 0.) { + const auto center = QPointF(pos) + + QPointF(size.width() / 2.0, size.height() / 2.0); + p.save(); + p.translate(center); + p.rotate(angle); + p.translate(-center); + e.icon.paint(p, pos, 0, context.textColor); + p.restore(); + } else { + e.icon.paint(p, pos, 0, context.textColor); + } +} + +void RefreshSpinEmoji::unload() { +} + +bool RefreshSpinEmoji::ready() { + return true; +} + +bool RefreshSpinEmoji::readyInDefaultState() { + return _phase == Phase::Idle; +} + +void RefreshSpinEmoji::handleLoading(bool now) { + if (now) { + if (_phase == Phase::Idle) { + _phase = Phase::Spinning; + _phaseStarted = crl::now(); + _animation.start(); + if (_repaint) { + _repaint(); + } + } + } else if (_phase == Phase::Waiting) { + _phase = Phase::Idle; + _animation.stop(); + if (_repaint) { + _repaint(); + } + } +} + +void RefreshSpinEmoji::tick(crl::time now) { + const auto elapsed = now - _phaseStarted; + if (_phase == Phase::Spinning && elapsed >= kSpinDuration) { + if (_loading.current()) { + _phase = Phase::Waiting; + _phaseStarted = now; + } else { + _phase = Phase::Idle; + _animation.stop(); + } + } else if (_phase == Phase::Waiting && elapsed >= kWaitDuration) { + if (_loading.current()) { + _phase = Phase::Spinning; + _phaseStarted = now; + } else { + _phase = Phase::Idle; + _animation.stop(); + } + } + if (_repaint) { + _repaint(); + } +} class PreviewAiToneExampleCard final : public Ui::RpWidget { public: - explicit PreviewAiToneExampleCard(QWidget *parent); + PreviewAiToneExampleCard( + QWidget *parent, + std::shared_ptr> loading); void showExample(Data::AiComposeToneExample example); void showSkeleton(bool shown); @@ -62,7 +206,9 @@ private: }; -PreviewAiToneExampleCard::PreviewAiToneExampleCard(QWidget *parent) +PreviewAiToneExampleCard::PreviewAiToneExampleCard( + QWidget *parent, + std::shared_ptr> loading) : RpWidget(parent) , _layout(Ui::CreateChild(this)) , _beforeTitle(_layout->add( @@ -113,7 +259,7 @@ PreviewAiToneExampleCard::PreviewAiToneExampleCard(QWidget *parent) st::aiTonePreviewExampleCardPadding.bottom()))) , _another(Ui::CreateChild( this, - tr::lng_ai_compose_tone_preview_add_example(), + rpl::single(QString()), st::aiTonePreviewAnotherExampleButton)) , _beforeSkeleton(_beforeBody) , _afterSkeleton(_afterBody) { @@ -137,6 +283,21 @@ PreviewAiToneExampleCard::PreviewAiToneExampleCard(QWidget *parent) titleGeometry.top() + shift, width); }, lifetime()); + auto context = Ui::Text::MarkedContext(); + context.repaint = [raw = _another.get()] { raw->update(); }; + context.customEmojiFactory = [loading = std::move(loading)]( + QStringView data, + const Ui::Text::MarkedContext &context + ) -> std::unique_ptr { + if (data != u"ai-tone-refresh"_q) { + return nullptr; + } + return std::make_unique(loading, context.repaint); + }; + _another->setContext(context); + _another->setText(rpl::single( + Ui::Text::SingleCustomEmoji(u"ai-tone-refresh"_q) + .append(tr::lng_ai_compose_tone_preview_add_example(tr::now)))); _another->setClickedCallback([=] { _anotherExampleRequested.fire({}); }); @@ -242,31 +403,32 @@ void PreviewAiToneBox( struct State { int examplesCount = 0; - bool requesting = false; + std::shared_ptr> requesting + = std::make_shared>(false); }; const auto state = box->lifetime().make_state(); state->examplesCount = tone.firstExample ? 1 : 0; const auto card = body->add( - object_ptr(body), + object_ptr(body, state->requesting), st::aiTonePreviewExampleCardMargin); const auto loadAnother = [=] { - if (state->requesting) { + if (state->requesting->current()) { return; } - state->requesting = true; + *state->requesting = true; card->showSkeleton(true); const auto num = state->examplesCount; session->data().aiComposeTones().getToneExample( tone, num, crl::guard(box, [=](Data::AiComposeToneExample example) { - state->requesting = false; + *state->requesting = false; ++state->examplesCount; card->showExample(std::move(example)); }), crl::guard(box, [=](const MTP::Error &) { - state->requesting = false; + *state->requesting = false; card->showSkeleton(false); box->showToast(tr::lng_ai_compose_error(tr::now)); })); From 22c8b5b72c87acf309529e98f31e6cbd30b155ff Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 22 Apr 2026 01:35:17 +0700 Subject: [PATCH 099/154] Update API scheme on layer 225. Restrict reactions. --- Telegram/Resources/langs/lang.strings | 2 ++ .../SourceFiles/boxes/peers/edit_peer_permissions_box.cpp | 2 ++ Telegram/SourceFiles/data/data_chat_participant_status.cpp | 2 ++ Telegram/SourceFiles/data/data_chat_participant_status.h | 1 + .../SourceFiles/history/admin_log/history_admin_log_item.cpp | 1 + Telegram/SourceFiles/history/history_item.cpp | 2 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 4 +++- 7 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4a5648fd86..27dc0a29d6 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6320,6 +6320,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_chat_send_media" = "Send media"; "lng_rights_chat_send_stickers" = "Send stickers & GIFs"; "lng_rights_chat_send_links" = "Embed links"; +"lng_rights_chat_send_reactions" = "Send reactions"; "lng_rights_chat_send_polls" = "Send polls"; "lng_rights_chat_add_members" = "Add members"; "lng_rights_chat_photos" = "Photos"; @@ -6620,6 +6621,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_banned_send_stickers" = "Send stickers & GIFs"; "lng_admin_log_banned_embed_links" = "Embed links"; "lng_admin_log_banned_send_polls" = "Send polls"; +"lng_admin_log_banned_send_reactions" = "Send reactions"; "lng_admin_log_admin_change_info" = "Change info"; "lng_admin_log_admin_post_messages" = "Post messages"; "lng_admin_log_admin_edit_messages" = "Edit messages"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 9707ab564e..3a89affff2 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -90,6 +90,7 @@ constexpr auto kDefaultChargeStars = 10; | Flag::SendGames | Flag::SendInline, tr::lng_rights_chat_stickers(tr::now) }, { Flag::EmbedLinks, tr::lng_rights_chat_send_links(tr::now) }, + { Flag::SendReactions, tr::lng_rights_chat_send_reactions(tr::now) }, { Flag::SendPolls, tr::lng_rights_chat_send_polls(tr::now) }, }; auto second = std::vector{ @@ -305,6 +306,7 @@ ChatRestrictions NegateRestrictions(ChatRestrictions value) { //| Flag::ViewMessages | Flag::ChangeInfo | Flag::EmbedLinks + | Flag::SendReactions | Flag::AddParticipants | Flag::CreateTopics | Flag::PinMessages diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 4e152fb818..d63c0acade 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -75,6 +75,7 @@ namespace { | (data.is_send_docs() ? Flag::SendFiles : Flag()) | (data.is_send_plain() ? Flag::SendOther : Flag()) | (data.is_embed_links() ? Flag::EmbedLinks : Flag()) + | (data.is_send_reactions() ? Flag::SendReactions : Flag()) | (data.is_change_info() ? Flag::ChangeInfo : Flag()) | (data.is_invite_users() ? Flag::AddParticipants : Flag()) | (data.is_pin_messages() ? Flag::PinMessages : Flag()) @@ -149,6 +150,7 @@ MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) { | ((flags & R::SendFiles) ? Flag::f_send_docs : Flag()) | ((flags & R::SendOther) ? Flag::f_send_plain : Flag()) | ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag()) + | ((flags & R::SendReactions) ? Flag::f_send_reactions : Flag()) | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) | ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag()) | ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag()) diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index 523b95bb24..013f27d4f7 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -66,6 +66,7 @@ enum class ChatRestriction { PinMessages = (1 << 17), CreateTopics = (1 << 18), EditRank = (1 << 26), + SendReactions = (1 << 27), }; inline constexpr bool is_flag_type(ChatRestriction) { return true; } using ChatRestrictions = base::flags; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 81e2e577d5..644f79008a 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -338,6 +338,7 @@ QString GeneratePermissionsChangeText( | Flag::SendInline | Flag::SendGames, tr::lng_admin_log_banned_send_stickers }, { Flag::EmbedLinks, tr::lng_admin_log_banned_embed_links }, + { Flag::SendReactions, tr::lng_admin_log_banned_send_reactions }, { Flag::SendPolls, tr::lng_admin_log_banned_send_polls }, { Flag::ChangeInfo, tr::lng_admin_log_admin_change_info }, { Flag::AddParticipants, tr::lng_admin_log_admin_invite_users }, diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 115b52201f..e37fa97a8c 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3137,7 +3137,7 @@ bool HistoryItem::canReact() const { return (_flags & MessageFlag::ReactionsAllowed); } } - return true; + return Data::CanSend(history()->peer, ChatRestriction::SendReactions, false); } void HistoryItem::addPaidReaction( diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 8cab9f6c22..daf117be96 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1246,7 +1246,7 @@ statsURL#47a971e0 url:string = StatsURL; chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true manage_ranks:flags.18?true = ChatAdminRights; -chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true edit_rank:flags.26?true until_date:int = ChatBannedRights; +chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true edit_rank:flags.26?true send_reactions:flags.27?true until_date:int = ChatBannedRights; inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; inputWallPaperSlug#72091c80 slug:string = InputWallPaper; @@ -2620,6 +2620,8 @@ messages.deletePollAnswer#ac8505a5 peer:InputPeer msg_id:int option:bytes = Upda messages.getUnreadPollVotes#43286cf2 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readPollVotes#1720b4d8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.setBotGuestChatResult#52b08db query_id:long result:InputBotInlineResult = Bool; +messages.deleteParticipantReactions#a0b80cf8 peer:InputPeer participant:InputPeer = Bool; +messages.deleteParticipantReaction#e3b7f82c peer:InputPeer msg_id:int participant:InputPeer = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; From 9821aa9615b7441a55a0ea3f56f450b1e1c519dd Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 22 Apr 2026 15:11:24 +0700 Subject: [PATCH 100/154] Suggest deleting all reactions in moderate box. --- Telegram/Resources/langs/lang.strings | 12 +- Telegram/SourceFiles/apiwrap.cpp | 22 + Telegram/SourceFiles/apiwrap.h | 7 + .../SourceFiles/boxes/delete_messages_box.cpp | 104 +--- .../SourceFiles/boxes/delete_messages_box.h | 10 +- .../boxes/moderate_messages_box.cpp | 504 +++++++++++++----- .../SourceFiles/boxes/moderate_messages_box.h | 13 +- .../history/history_inner_widget.cpp | 11 +- .../SourceFiles/history/history_widget.cpp | 6 +- .../view/history_view_chat_section.cpp | 2 +- .../view/history_view_context_menu.cpp | 11 +- .../history/view/history_view_list_widget.cpp | 7 +- .../view/history_view_scheduled_section.cpp | 2 +- .../media/view/media_view_overlay_widget.cpp | 20 +- .../business/settings_shortcut_messages.cpp | 2 +- .../SourceFiles/window/window_peer_menu.cpp | 6 +- 16 files changed, 467 insertions(+), 272 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 27dc0a29d6..443cf8066d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4578,7 +4578,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ban_users" = "Ban users"; "lng_restrict_users" = "Restrict users"; "lng_delete_all_from_user" = "Delete all from {user}"; -"lng_delete_all_from_users" = "Delete all from users"; "lng_restrict_user#one" = "Restrict user"; "lng_restrict_user#other" = "Restrict users"; "lng_restrict_user_part" = "Partially restrict this user {emoji}"; @@ -5547,6 +5546,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_selected_delete_sure_this" = "Do you want to delete this message?"; "lng_selected_delete_sure#one" = "Do you want to delete {count} message?"; "lng_selected_delete_sure#other" = "Do you want to delete {count} messages?"; +"lng_delete_title_message_one" = "Delete this message"; +"lng_delete_title_message_many#one" = "Delete {count} message"; +"lng_delete_title_message_many#other" = "Delete {count} messages"; +"lng_delete_title_reaction_this" = "Delete this reaction"; +"lng_delete_title_reaction_all" = "Delete all reactions"; +"lng_delete_label_also_this_reaction" = "Also delete this reaction."; +"lng_delete_label_also_some_reactions" = "Also delete all reactions from some participants."; +"lng_delete_label_also_all_reactions" = "Also delete all reactions."; +"lng_delete_sub_messages" = "Delete all messages"; +"lng_delete_sub_reactions" = "Delete all reactions"; +"lng_context_delete_this_reaction" = "Delete this reaction"; "lng_selected_remove_saved_music" = "Do you want to remove this file from your profile?"; "lng_saved_music_added" = "Audio added to your Profile."; "lng_saved_music_removed" = "Audio removed from your Profile."; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 03c9e7ec49..e3f4a13330 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -1422,6 +1422,28 @@ void ApiWrap::deleteAllFromParticipantSend( }).send(); } +void ApiWrap::deleteAllReactionsFromParticipant( + not_null peer, + not_null participant) { + request(MTPmessages_DeleteParticipantReactions( + peer->input(), + participant->input() + )).send(); +} + +void ApiWrap::deleteParticipantReaction( + not_null peer, + MsgId msgId, + not_null participant) { + request(MTPmessages_DeleteParticipantReaction( + peer->input(), + MTP_int(msgId.bare), + participant->input() + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + }).send(); +} + void ApiWrap::deleteSublistHistory( not_null channel, not_null sublistPeer) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index c57e4976d8..dd98d792a7 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -237,6 +237,13 @@ public: void deleteAllFromParticipant( not_null channel, not_null from); + void deleteAllReactionsFromParticipant( + not_null peer, + not_null participant); + void deleteParticipantReaction( + not_null peer, + MsgId msgId, + not_null participant); void deleteSublistHistory( not_null parentChat, not_null sublistPeer); diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp index 7e1331901a..526bbf3423 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp @@ -8,9 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/delete_messages_box.h" #include "apiwrap.h" -#include "api/api_chat_participants.h" -#include "api/api_messages_search.h" -#include "api/api_report.h" #include "base/unixtime.h" #include "core/application.h" #include "core/core_settings.h" @@ -38,18 +35,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL DeleteMessagesBox::DeleteMessagesBox( QWidget*, - not_null item, - bool suggestModerateActions) + not_null item) : _session(&item->history()->session()) , _ids(1, item->fullId()) { - if (suggestModerateActions) { - _moderateBan = item->suggestBanReport(); - _moderateDeleteAll = item->suggestDeleteAllReport(); - if (_moderateBan || _moderateDeleteAll) { - _moderateFrom = item->from(); - _moderateInChannel = item->history()->peer->asChannel(); - } - } } DeleteMessagesBox::DeleteMessagesBox( @@ -178,53 +166,6 @@ void DeleteMessagesBox::prepare() { tr::lng_delete_clear_for_me(tr::now) }); } - } else if (_moderateFrom) { - Assert(_moderateInChannel != nullptr); - - details.text = tr::lng_selected_delete_sure_this(tr::now); - if (_moderateBan) { - _banUser.create( - this, - tr::lng_ban_user(tr::now), - false, - st::defaultBoxCheckbox); - } - _reportSpam.create( - this, - tr::lng_report_spam(tr::now), - false, - st::defaultBoxCheckbox); - if (_moderateDeleteAll) { - const auto search = lifetime().make_state( - _session->data().message(_ids.front())->history()); - _deleteAll.create( - this, - tr::lng_delete_all_from_user( - tr::now, - lt_user, - tr::bold(_moderateFrom->name()), - tr::marked), - false, - st::defaultBoxCheckbox); - - *deleteText = rpl::combine( - rpl::single( - 0 - ) | rpl::then( - search->messagesFounds( - ) | rpl::map([](const Api::FoundMessages &found) { - return found.total; - }) - ), - _deleteAll->checkedValue() - ) | rpl::map([](int total, bool checked) { - return tr::lng_box_delete(tr::now) - + ((total <= 0 || !checked) - ? QString() - : QString(" (%1)").arg(total)); - }); - search->searchMessages({ .from = _moderateFrom }); - } } else { details.text = hasSavedMusicMessages() ? tr::lng_selected_remove_saved_music(tr::now) @@ -327,16 +268,7 @@ void DeleteMessagesBox::prepare() { auto fullHeight = st::boxPadding.top() + _text->height() + st::boxPadding.bottom(); - if (_moderateFrom) { - fullHeight += st::boxMediumSkip; - if (_banUser) { - fullHeight += _banUser->heightNoMargins() + st::boxLittleSkip; - } - fullHeight += _reportSpam->heightNoMargins(); - if (_deleteAll) { - fullHeight += st::boxLittleSkip + _deleteAll->heightNoMargins(); - } - } else if (_revoke) { + if (_revoke) { fullHeight += st::boxMediumSkip + _revoke->heightNoMargins(); } if (_autoDeleteSettings) { @@ -475,20 +407,7 @@ void DeleteMessagesBox::resizeEvent(QResizeEvent *e) { const auto &padding = st::boxPadding; _text->moveToLeft(padding.left(), padding.top()); auto top = _text->bottomNoMargins() + st::boxMediumSkip; - if (_moderateFrom) { - if (_banUser) { - _banUser->moveToLeft(padding.left(), top); - top += _banUser->heightNoMargins() + st::boxLittleSkip; - } - _reportSpam->moveToLeft(padding.left(), top); - top += _reportSpam->heightNoMargins() + st::boxLittleSkip; - if (_deleteAll) { - const auto availableWidth = width() - 2 * padding.left(); - _deleteAll->resizeToNaturalWidth(availableWidth); - _deleteAll->moveToLeft(padding.left(), top); - top += _deleteAll->heightNoMargins() + st::boxLittleSkip; - } - } else if (_revoke) { + if (_revoke) { const auto availableWidth = width() - 2 * padding.left(); _revoke->resizeToNaturalWidth(availableWidth); _revoke->moveToLeft(padding.left(), top); @@ -625,23 +544,6 @@ void DeleteMessagesBox::deleteAndClear() { } return; } - if (_moderateFrom) { - if (_banUser && _banUser->checked()) { - _moderateInChannel->session().api().chatParticipants().kick( - _moderateInChannel, - _moderateFrom, - ChatRestrictionsInfo()); - } - if (_reportSpam->checked()) { - Api::ReportSpam(_moderateFrom, { _ids[0] }); - } - if (_deleteAll && _deleteAll->checked()) { - _moderateInChannel->session().api().deleteAllFromParticipant( - _moderateInChannel, - _moderateFrom); - } - } - const auto ids = _ids; invokeCallbackAndClose(); session->data().histories().deleteMessages(ids, revoke); diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.h b/Telegram/SourceFiles/boxes/delete_messages_box.h index 635a1fd362..686c69993c 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.h +++ b/Telegram/SourceFiles/boxes/delete_messages_box.h @@ -27,8 +27,7 @@ class DeleteMessagesBox final : public Ui::BoxContent { public: DeleteMessagesBox( QWidget*, - not_null item, - bool suggestModerateActions); + not_null item); DeleteMessagesBox( QWidget*, not_null session, @@ -70,10 +69,6 @@ private: const QDate _wipeHistoryFirstToDelete; const QDate _wipeHistoryLastToDelete; const MessageIdsList _ids; - PeerData *_moderateFrom = nullptr; - ChannelData *_moderateInChannel = nullptr; - bool _moderateBan = false; - bool _moderateDeleteAll = false; bool _revokeForBot = false; bool _revokeJustClearForChannel = false; @@ -81,9 +76,6 @@ private: object_ptr _text = { nullptr }; object_ptr _revoke = { nullptr }; object_ptr> _revokeRemember = { nullptr }; - object_ptr _banUser = { nullptr }; - object_ptr _reportSpam = { nullptr }; - object_ptr _deleteAll = { nullptr }; object_ptr _autoDeleteSettings = { nullptr }; int _fullHeight = 0; diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 2c6d7c510d..0685274b33 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -349,15 +349,29 @@ void ProccessCommonGroups( void CreateModerateMessagesBox( not_null box, - const HistoryItemsList &items, + ModerateMessagesBoxEntry entry, Fn confirmed, ModerateMessagesBoxOptions options) { - Expects(!items.empty()); + const auto &items = entry.items; + const auto reaction = entry.reaction; + Expects(!items.empty() || reaction.has_value()); + + const auto hasItems = !items.empty(); + const auto hasReaction = reaction.has_value(); + const auto itemsCount = hasItems ? int(items.size()) : 0; using Controller = Ui::ExpandablePeerListController; - const auto [allCanBan, allCanDelete, participants] - = CalculateModerateOptions(items); + const auto moderateOptions = hasItems + ? CalculateModerateOptions(items) + : ModerateOptions{ + .allCanBan = false, + .allCanDelete = false, + .participants = { reaction->participant }, + }; + const auto allCanBan = moderateOptions.allCanBan; + const auto allCanDelete = moderateOptions.allCanDelete; + const auto &participants = moderateOptions.participants; const auto inner = box->verticalLayout(); Assert(!participants.empty()); @@ -375,14 +389,45 @@ void CreateModerateMessagesBox( participants.size()).width(), 0); - const auto itemsCount = int(items.size()); - const auto firstItem = items.front(); - const auto history = firstItem->history(); - const auto session = &history->session(); - const auto historyPeerId = history->peer->id; - const auto ids = session->data().itemsToIds(items); + const auto firstItem = hasItems ? items.front().get() : nullptr; + const auto session = hasItems + ? &firstItem->history()->session() + : &reaction->peer->session(); + const auto history = hasItems + ? firstItem->history().get() + : session->data().historyLoaded(reaction->peer); + const auto historyPeerId = hasItems + ? history->peer->id + : reaction->peer->id; + const auto ids = hasItems + ? session->data().itemsToIds(items) + : MessageIdsList(); + const auto selectedMessagesByParticipant = [&] { + auto result = base::flat_map(); + if (!hasItems) { + return result; + } + for (const auto &item : items) { + const auto from = item->from(); + if (!from) { + continue; + } + const auto i = result.find(from->id); + if (i == result.end()) { + result.emplace(from->id, 1); + } else { + ++i->second; + } + } + return result; + }(); + const auto participantIds = ranges::views::all( + participants + ) | ranges::views::transform([](not_null peer) { + return peer->id; + }) | ranges::to_vector; - { + if (hasItems) { const auto remainingIds = box->lifetime().make_state>( ids.begin(), @@ -396,7 +441,8 @@ void CreateModerateMessagesBox( }, box->lifetime()); } - if (ModerateCommonGroups.value() || session->supportMode()) { + if (hasItems + && (ModerateCommonGroups.value() || session->supportMode())) { ProccessCommonGroups( items, crl::guard(box, [=](CommonGroups groups, not_null user) { @@ -564,21 +610,51 @@ void CreateModerateMessagesBox( return base::EventFilterResult::Cancel; }); }; + Ui::Checkbox *deleteMessages = nullptr; + Controller *deleteMessagesController = nullptr; + rpl::variable> *deleteMessagesCounts = nullptr; + Ui::Checkbox *deleteReactions = nullptr; + Controller *deleteReactionsController = nullptr; + const auto effectiveCheckedParticipants = []( + Ui::Checkbox *checkbox, + Controller *controller) { + if (!checkbox || !controller || !controller->collectRequests) { + return Participants(); + } else if (!checkbox->checked() + && (controller->data.participants.size() == 1)) { + return Participants(); + } + return controller->collectRequests(); + }; + const auto checkedParticipantsValue = [=]( + not_null checkbox, + not_null controller) + -> rpl::producer { + if (controller->data.participants.size() == 1) { + return checkbox->checkedValue() | rpl::map([=](bool) { + return effectiveCheckedParticipants(checkbox, controller); + }); + } + return rpl::merge( + rpl::single(false), + controller->checkAllRequests.events(), + controller->toggleRequestsFromInner.events() + ) | rpl::map([=](bool) { + return effectiveCheckedParticipants(checkbox, controller); + }); + }; - Ui::AddSkip(inner); - const auto title = box->addRow( - object_ptr( + const auto subtitle = box->addRow( + object_ptr>( box, - (itemsCount == 1) - ? tr::lng_selected_delete_sure_this() - : tr::lng_selected_delete_sure( - lt_count, - rpl::single(itemsCount) | tr::to_count()), - st::boxLabel)); + object_ptr( + box, + QString(), + st::boxLabel))); + subtitle->entity()->setTextColorOverride(st::windowSubTextFg->c); + subtitle->hide(anim::type::instant); Ui::AddSkip(inner); - Ui::AddSkip(inner); - Ui::AddSkip(inner); - { + if (hasItems) { const auto report = box->addRow( object_ptr( box, @@ -598,113 +674,263 @@ void CreateModerateMessagesBox( }); } - if (allCanDelete) { + const auto showMessagesCheckbox = allCanDelete && hasItems; + const auto showReactionsCheckbox = (allCanDelete && hasItems) + || (hasReaction && !hasItems); + if (showMessagesCheckbox || showReactionsCheckbox) { Ui::AddSkip(inner); Ui::AddSkip(inner); + const auto checkedParticipants = options.deleteAll + ? participantIds + : std::vector(); - const auto deleteAll = inner->add( - object_ptr( - inner, - !(isSingle) - ? tr::lng_delete_all_from_users( - tr::now, - tr::marked) - : tr::lng_delete_all_from_user( - tr::now, - lt_user, - tr::bold(firstItem->from()->name()), - tr::marked), - options.deleteAll, - st::defaultBoxCheckbox), - st::boxRowPadding + buttonPadding); - auto messagesCounts = MessagesCountValue(history, participants); - - const auto controller = box->lifetime().make_state( - Controller::Data{ - .messagesCounts = rpl::duplicate(messagesCounts), - .participants = participants, + if (showMessagesCheckbox) { + deleteMessagesCounts = box->lifetime().make_state< + rpl::variable>>( + base::flat_map()); + MessagesCountValue( + history, + participants + ) | rpl::on_next([=](base::flat_map counts) { + deleteMessagesCounts->force_assign(std::move(counts)); + }, box->lifetime()); + deleteMessagesController = box->lifetime().make_state( + Controller::Data{ + .messagesCounts = deleteMessagesCounts->value(), + .participants = participants, + .checked = checkedParticipants, + }); + deleteMessages = inner->add( + object_ptr( + inner, + tr::lng_delete_sub_messages(tr::now), + options.deleteAll, + st::defaultBoxCheckbox), + st::boxRowPadding + buttonPadding); + Ui::AddExpandablePeerList( + not_null{ deleteMessages }, + not_null{ deleteMessagesController }, + inner); + handleSubmition(not_null{ deleteMessages }); + handleConfirmation( + not_null{ deleteMessages }, + not_null{ deleteMessagesController }, + [=]( + not_null p, + not_null c) { + p->session().api().deleteAllFromParticipant(c, p); }); - Ui::AddExpandablePeerList(deleteAll, controller, inner); - { - auto itemFromIds = items | ranges::views::transform([]( - const auto &item) { - return item->from()->id; - }) | ranges::to_vector; - - rpl::combine( - std::move(messagesCounts), - isSingle - ? deleteAll->checkedValue() - : rpl::merge( - controller->toggleRequestsFromInner.events(), - controller->checkAllRequests.events()) - ) | rpl::map([=](const auto &map, bool c) { - const auto checked = (isSingle && !c) - ? Participants() - : controller->collectRequests - ? controller->collectRequests() - : Participants(); - auto result = 0; - for (const auto &[peerId, count] : map) { - for (const auto &peer : checked) { - if (peer->id == peerId) { - result += count; - break; - } - } - } - for (const auto &fromId : itemFromIds) { - for (const auto &peer : checked) { - if (peer->id == fromId) { - result--; - break; - } - } - result++; - } - return float64(result); - }) | rpl::on_next([=](int amount) { - auto text = tr::lng_selected_delete_sure( - tr::now, - lt_count, - float64(amount)); - if (amount > 0) { - title->setText(std::move(text)); - } else { - const auto zeroIndex = text.indexOf('0'); - if (zeroIndex != -1) { - auto descriptor = Lottie::IconDescriptor{ - .name = u"transcribe_loading"_q, - .color = &st::attentionButtonFg, // Any contrast. - .sizeOverride = Size( - st::historyTranscribeLoadingSize), - .colorizeUsingAlpha = true, - }; - auto result = TextWithEntities() - .append(text.mid(0, zeroIndex)) - .append(Ui::Text::LottieEmoji(descriptor)) - .append(text.mid(zeroIndex + 1)); - using namespace Ui::Text; - title->setMarkedText( - std::move(result), - LottieEmojiContext(std::move(descriptor))); - } else { - title->setText(std::move(text)); - } - } - title->resizeToWidth(inner->width() - - rect::m::sum::h(st::boxRowPadding)); - }, title->lifetime()); } - handleSubmition(deleteAll); - handleConfirmation(deleteAll, controller, [=]( - not_null p, - not_null c) { - p->session().api().deleteAllFromParticipant(c, p); - }); + if (deleteMessages && showReactionsCheckbox) { + Ui::AddSkip(inner); + Ui::AddSkip(inner); + } + + if (showReactionsCheckbox) { + deleteReactionsController = box->lifetime().make_state( + Controller::Data{ + .participants = participants, + .checked = checkedParticipants, + }); + deleteReactions = inner->add( + object_ptr( + inner, + tr::lng_delete_sub_reactions(tr::now), + options.deleteAll, + st::defaultBoxCheckbox), + st::boxRowPadding + buttonPadding); + Ui::AddExpandablePeerList( + not_null{ deleteReactions }, + not_null{ deleteReactionsController }, + inner); + handleSubmition(not_null{ deleteReactions }); + confirms->events() | rpl::on_next([=] { + if (deleteReactions->checked() + && deleteReactionsController->collectRequests + && !effectiveCheckedParticipants( + deleteReactions, + deleteReactionsController).empty()) { + const auto peer = hasItems + ? history->peer.get() + : reaction->peer.get(); + for (const auto &participant + : deleteReactionsController->collectRequests()) { + peer->session().api() + .deleteAllReactionsFromParticipant( + peer, + participant); + } + } + }, deleteReactions->lifetime()); + } } - if (allCanBan) { + const auto makeTitleLoadingDescriptor = [] { + return Lottie::IconDescriptor{ + .name = u"transcribe_loading"_q, + .color = &st::attentionButtonFg, + .sizeOverride = Size(st::historyTranscribeLoadingSize), + .colorizeUsingAlpha = true, + }; + }; + const auto titleLoadingEmojiData = Ui::Text::LottieEmojiData( + makeTitleLoadingDescriptor()); + struct MessageTitleData final { + int count = 0; + bool resolved = false; + }; + const auto langUpdated = rpl::single( + 0 + ) | rpl::then(Lang::Updated() | rpl::map([] { + return 0; + })); + const auto makeMessageTitleData = [=]( + const base::flat_map &messagesCounts, + const Participants &checked) { + auto result = MessageTitleData{ + .count = itemsCount, + .resolved = true, + }; + for (const auto &peer : checked) { + const auto i = messagesCounts.find(peer->id); + if (i == end(messagesCounts)) { + result.resolved = false; + } else { + result.count += i->second; + } + if (const auto j = selectedMessagesByParticipant.find(peer->id); + j != end(selectedMessagesByParticipant)) { + result.count -= j->second; + } + } + return result; + }; + auto title = [&]() -> rpl::producer { + if (hasItems && showMessagesCheckbox) { + auto messageTitleData = rpl::combine( + deleteMessagesCounts->value(), + checkedParticipantsValue( + not_null{ deleteMessages }, + not_null{ deleteMessagesController }) + ) | rpl::map(makeMessageTitleData); + return rpl::combine( + std::move(messageTitleData), + rpl::duplicate(langUpdated) + ) | rpl::map([=](const MessageTitleData &data, int) { + const auto count = data.count; + const auto resolved = data.resolved; + const auto text = (count == 1) + ? tr::lng_delete_title_message_one(tr::now) + : tr::lng_delete_title_message_many( + tr::now, + lt_count, + count); + if (resolved || count != 0) { + return TextWithEntities{ text }; + } + const auto zeroIndex = text.indexOf('0'); + return (zeroIndex == -1) + ? TextWithEntities{ text } + : TextWithEntities() + .append(text.mid(0, zeroIndex)) + .append(Ui::Text::LottieEmoji( + makeTitleLoadingDescriptor())) + .append(text.mid(zeroIndex + 1)); + }); + } else if (hasItems) { + return rpl::duplicate(langUpdated) | rpl::map([=](int) { + return (itemsCount == 1) + ? TextWithEntities{ + tr::lng_delete_title_message_one(tr::now) + } + : TextWithEntities{ + tr::lng_delete_title_message_many( + tr::now, + lt_count, + itemsCount) + }; + }); + } else if (deleteReactions) { + return rpl::combine( + deleteReactions->checkedValue(), + rpl::duplicate(langUpdated) + ) | rpl::map([=](bool checked, int) { + return TextWithEntities{ checked + ? tr::lng_delete_title_reaction_all(tr::now) + : tr::lng_delete_title_reaction_this(tr::now) }; + }); + } + return rpl::duplicate(langUpdated) | rpl::map([](int) { + return TextWithEntities{ + tr::lng_delete_title_reaction_this(tr::now) + }; + }); + }(); + auto titleContext = Core::TextContext({ .session = session }); + auto titleFactory = std::move(titleContext.customEmojiFactory); + titleContext.customEmojiFactory = [ + titleFactory = std::move(titleFactory), + titleLoadingEmojiData, + makeTitleLoadingDescriptor + ](QStringView data, const Ui::Text::MarkedContext &context) + -> std::unique_ptr { + if (data == titleLoadingEmojiData) { + return std::make_unique( + makeTitleLoadingDescriptor(), + context.repaint); + } + return titleFactory(data, context); + }; + box->getDelegate()->setTitle(std::move(title), std::move(titleContext)); + enum class SubtitleKind { + None, + ThisReaction, + SomeReactions, + AllReactions, + }; + if (hasItems) { + const auto subtitleKind = box->lifetime().make_state< + rpl::variable>(SubtitleKind::None); + auto reactionsCheckedValue = showReactionsCheckbox + ? checkedParticipantsValue( + not_null{ deleteReactions }, + not_null{ deleteReactionsController }) + : rpl::single(Participants()); + rpl::combine( + subtitleKind->value(), + rpl::duplicate(langUpdated) + ) | rpl::map([](SubtitleKind kind, int) { + switch (kind) { + case SubtitleKind::ThisReaction: + return tr::lng_delete_label_also_this_reaction(tr::now); + case SubtitleKind::SomeReactions: + return tr::lng_delete_label_also_some_reactions(tr::now); + case SubtitleKind::AllReactions: + return tr::lng_delete_label_also_all_reactions(tr::now); + case SubtitleKind::None: + return QString(); + } + Unexpected("Bad subtitle kind."); + }) | rpl::on_next([=](const QString &text) { + subtitle->entity()->setText(text); + }, subtitle->lifetime()); + rpl::combine( + std::move(reactionsCheckedValue), + rpl::single(hasReaction) + ) | rpl::on_next([=](const Participants &checked, bool hasReaction) { + auto kind = SubtitleKind::None; + if (!checked.empty()) { + kind = (checked.size() == participants.size()) + ? SubtitleKind::AllReactions + : SubtitleKind::SomeReactions; + } else if (hasReaction) { + kind = SubtitleKind::ThisReaction; + } + subtitleKind->force_assign(kind); + subtitle->toggle(kind != SubtitleKind::None, anim::type::normal); + }, subtitle->lifetime()); + } + if (hasItems && allCanBan) { const auto peer = items.front()->history()->peer; auto ownedWrap = peer->isMonoforum() ? nullptr @@ -902,19 +1128,23 @@ void CreateModerateMessagesBox( } const auto close = crl::guard(box, [=] { box->closeBox(); }); - { - const auto data = &participants.front()->session().data(); - const auto ids = data->itemsToIds(items); - box->addButton(tr::lng_box_delete(), [=] { - confirms->fire({}); - if (confirmed) { - confirmed(); - } - data->histories().deleteMessages(ids, true); - data->sendHistoryChangeNotifications(); - close(); - }); - } + box->addButton(tr::lng_box_delete(), [=] { + confirms->fire({}); + if (confirmed) { + confirmed(); + } + if (hasItems) { + session->data().histories().deleteMessages(ids, true); + session->data().sendHistoryChangeNotifications(); + } + if (reaction) { + session->api().deleteParticipantReaction( + reaction->peer, + reaction->msgId, + reaction->participant); + } + close(); + }); box->addButton(tr::lng_cancel(), close); } diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.h b/Telegram/SourceFiles/boxes/moderate_messages_box.h index 2d5f2cec1c..1847d60ff1 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.h +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.h @@ -25,11 +25,22 @@ struct ModerateMessagesBoxOptions final { bool banUser = false; }; +struct ModerateReactionEntry { + not_null peer; + MsgId msgId; + not_null participant; +}; + +struct ModerateMessagesBoxEntry { + HistoryItemsList items; + std::optional reaction; +}; + [[nodiscard]] ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions(); void CreateModerateMessagesBox( not_null box, - const HistoryItemsList &items, + ModerateMessagesBoxEntry entry, Fn confirmed, ModerateMessagesBoxOptions options); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 858ef7487c..455548d4ef 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -5270,10 +5270,13 @@ void HistoryInner::deleteItem(not_null item) { const auto list = HistoryItemsList{ item }; if (CanCreateModerateMessagesBox(list)) { const auto opt = DefaultModerateMessagesBoxOptions(); - _controller->show(Box(CreateModerateMessagesBox, list, nullptr, opt)); + _controller->show(Box( + CreateModerateMessagesBox, + ModerateMessagesBoxEntry{ .items = list }, + nullptr, + opt)); } else { - const auto suggestModerate = false; - _controller->show(Box(item, suggestModerate)); + _controller->show(Box(item)); } } @@ -5290,7 +5293,7 @@ void HistoryInner::deleteAsGroup(FullMsgId itemId) { } else if (CanCreateModerateMessagesBox(group->items)) { _controller->show(Box( CreateModerateMessagesBox, - group->items, + ModerateMessagesBoxEntry{ .items = group->items }, nullptr, ModerateMessagesBoxOptions{})); } else { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index d3f51ae454..08b9b3d9d4 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4799,9 +4799,7 @@ void HistoryWidget::saveEditMessage(Api::SendOptions options) { || !webPageDraft.manual) && !hasMediaWithCaption) { if (item->computeSuggestionActions() == SuggestionActions::None) { - const auto suggestModerateActions = false; - controller()->show( - Box(item, suggestModerateActions)); + controller()->show(Box(item)); } return; } else { @@ -9590,7 +9588,7 @@ void HistoryWidget::confirmDeleteSelected() { const auto opt = DefaultModerateMessagesBoxOptions(); controller()->show(Box( CreateModerateMessagesBox, - items, + ModerateMessagesBoxEntry{ .items = items }, crl::guard(this, [=] { clearSelected(); }), opt)); } else { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 92f92f2a0c..1d75b715b0 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1507,7 +1507,7 @@ void ChatWidget::edit( && item->media()->allowsEditCaption(); if (sending.text.isEmpty() && !hasMediaWithCaption) { if (item) { - controller()->show(Box(item, false)); + controller()->show(Box(item)); } else { doSetInnerFocus(); } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 8d26f0dacb..bc77c7bac7 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -913,12 +913,13 @@ bool AddDeleteMessageAction( const auto list = HistoryItemsList{ item }; if (CanCreateModerateMessagesBox(list)) { const auto opt = DefaultModerateMessagesBoxOptions(); - controller->show( - Box(CreateModerateMessagesBox, list, nullptr, opt)); + controller->show(Box( + CreateModerateMessagesBox, + ModerateMessagesBoxEntry{ .items = list }, + nullptr, + opt)); } else { - const auto suggestModerateActions = false; - controller->show( - Box(item, suggestModerateActions)); + controller->show(Box(item)); } } }); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 9f40a25ec3..a2c0028e37 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -4658,8 +4658,11 @@ void ConfirmDeleteSelectedItems(not_null widget) { }); if (CanCreateModerateMessagesBox(historyItems)) { const auto opt = DefaultModerateMessagesBoxOptions(); - controller->show( - Box(CreateModerateMessagesBox, historyItems, confirmed, opt)); + controller->show(Box( + CreateModerateMessagesBox, + ModerateMessagesBoxEntry{ .items = historyItems }, + confirmed, + opt)); } else { auto box = Box( &widget->session(), diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 1d0133f9d8..97dea95670 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -772,7 +772,7 @@ void ScheduledWidget::edit( && item->media()->allowsEditCaption(); if (sending.text.isEmpty() && !hasMediaWithCaption) { if (item) { - controller()->show(Box(item, false)); + controller()->show(Box(item)); } else { _composeControls->focus(); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 82afd73c98..cbe784bdb1 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "info/statistics/info_statistics_widget.h" #include "boxes/delete_messages_box.h" +#include "boxes/moderate_messages_box.h" #include "boxes/report_messages_box.h" #include "media/audio/media_audio.h" #include "media/view/media_view_group_thumbs.h" @@ -3339,10 +3340,21 @@ void OverlayWidget::deleteMedia() { Ui::LayerOption::CloseOther); } } else if (message) { - const auto suggestModerateActions = true; - window->show( - Box(message, suggestModerateActions), - Ui::LayerOption::CloseOther); + const auto list = HistoryItemsList{ message }; + if (CanCreateModerateMessagesBox(list)) { + const auto opt = DefaultModerateMessagesBoxOptions(); + window->show( + Box( + CreateModerateMessagesBox, + ModerateMessagesBoxEntry{ .items = list }, + nullptr, + opt), + Ui::LayerOption::CloseOther); + } else { + window->show( + Box(message), + Ui::LayerOption::CloseOther); + } } } } diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 2c5aef1083..aa47d3bc3f 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -1235,7 +1235,7 @@ void ShortcutMessages::edit( if (!TextUtilities::CutPart(sending, left, maxCaptionSize) && !hasMediaWithCaption) { if (item) { - _controller->show(Box(item, false)); + _controller->show(Box(item)); } else { doSetInnerFocus(); } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index e1c095eae9..a4587df2e8 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -4109,7 +4109,11 @@ void AddSenderUserpicModerateAction( } controller->show(Box( CreateModerateMessagesBox, - HistoryItemsList{ not_null(item) }, + ModerateMessagesBoxEntry{ + .items = HistoryItemsList{ + not_null(item), + }, + }, nullptr, ModerateMessagesBoxOptions{ .reportSpam = true, From bff17504bc96df235559211d97ea616506f22889 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 21 Apr 2026 20:05:34 +0700 Subject: [PATCH 101/154] Update API scheme on layer 225. --- Telegram/Resources/langs/lang.strings | 5 ++ .../SourceFiles/boxes/create_ai_tone_box.cpp | 66 +++++++++++++++++++ .../SourceFiles/boxes/create_ai_tone_box.h | 4 ++ .../SourceFiles/boxes/preview_ai_tone_box.cpp | 45 +++++++++++-- .../data/data_ai_compose_tones.cpp | 17 +++-- .../SourceFiles/data/data_ai_compose_tones.h | 8 ++- .../SourceFiles/data/data_premium_limits.cpp | 7 ++ .../SourceFiles/data/data_premium_limits.h | 3 + Telegram/SourceFiles/mtproto/scheme/api.tl | 2 +- 9 files changed, 141 insertions(+), 16 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 443cf8066d..c691e21866 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7964,6 +7964,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_preview_created_by" = "Created by {user}"; "lng_ai_compose_tone_added" = "Style Added"; "lng_ai_compose_tone_added_description" = "Tap \"AI\" → \"{name}\" when typing your next long message."; +"lng_ai_compose_tone_saved_limit#one" = "Subscribe to {link} to save up to {premium_count} styles, or delete one of your **{count}** style to add another."; +"lng_ai_compose_tone_saved_limit#other" = "Subscribe to {link} to save up to {premium_count} styles, or delete one of your **{count}** styles to add another."; +"lng_ai_compose_tone_saved_limit_link" = "Premium"; +"lng_ai_compose_tone_saved_limit_final#one" = "You can save up to **{count}** style. Delete one to add another."; +"lng_ai_compose_tone_saved_limit_final#other" = "You can save up to **{count}** styles. Delete one to add another."; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index 522ef60cdb..f378d209d7 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -14,11 +14,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_forum_icons.h" +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/stickers/data_custom_emoji.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" +#include "main/session/session_show.h" +#include "main/main_app_config.h" #include "main/main_session.h" +#include "settings/sections/settings_premium.h" #include "ui/abstract_button.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/custom_emoji_toast_icon.h" @@ -345,6 +349,9 @@ void SetupToneBox( rpl::producer(), initialName), st::aiToneFieldsMargin); + name->setMaxLength(session->appConfig().get( + u"aicompose_tone_title_length_max"_q, + 12)); Ui::AddSkip(container, st::aiToneFieldsSkip); @@ -380,6 +387,9 @@ void SetupToneBox( initialPrompt), st::aiToneFieldsMargin); prompt->setSubmitSettings(Ui::InputField::SubmitSettings::None); + prompt->setMaxLength(session->appConfig().get( + u"aicompose_tone_prompt_length_max"_q, + 1024)); struct FieldDecor { not_null bg; @@ -587,6 +597,13 @@ void CreateAiToneBox( if (saved) { saved(tone); } + }), + crl::guard(box, [=](const MTP::Error &error) { + if (error.type() == u"TONES_SAVED_TOO_MANY"_q) { + ShowAiComposeToneLimitError(box->uiShow(), session); + } else { + box->showToast(tr::lng_ai_compose_error(tr::now)); + } })); }, nullptr); @@ -661,3 +678,52 @@ void ConfirmDeleteAiTone( .title = tr::lng_ai_compose_tone_delete(), })); } + +void ShowAiComposeToneLimitError( + std::shared_ptr show, + not_null session) { + const auto limits = Data::PremiumLimits(session); + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + const auto defaultLimit = limits.aiComposeSavedTonesDefault(); + const auto premiumLimit = limits.aiComposeSavedTonesPremium(); + const auto current = premium ? premiumLimit : defaultLimit; + if (premium || !premiumPossible) { + using WeakToast = base::weak_ptr; + const auto toast = std::make_shared(); + (*toast) = show->showToast({ + .text = tr::lng_ai_compose_tone_saved_limit_final( + tr::now, + lt_count, + current, + tr::rich), + .filter = crl::guard(session, [=]( + const ClickHandlerPtr &, + Qt::MouseButton button) { + if (button == Qt::LeftButton) { + if (const auto strong = toast->get()) { + strong->hideAnimated(); + (*toast) = nullptr; + } + } + return true; + }), + }); + } else { + Settings::ShowPremiumPromoToast( + Main::MakeSessionShow(show, session), + ChatHelpers::ResolveWindowDefault(), + tr::lng_ai_compose_tone_saved_limit( + tr::now, + lt_count, + defaultLimit, + lt_link, + tr::bold(tr::lng_ai_compose_tone_saved_limit_link( + tr::now, + tr::link)), + lt_premium_count, + tr::bold(QString::number(premiumLimit)), + tr::rich), + u"ai_compose_tones"_q); + } +} diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h index d91daaf6a7..4760eeaacb 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.h +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -44,3 +44,7 @@ void ConfirmDeleteAiTone( not_null session, const Data::AiComposeTone &tone, Fn done = nullptr); + +void ShowAiComposeToneLimitError( + std::shared_ptr show, + not_null session); diff --git a/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp index a5374c3a67..74c09972af 100644 --- a/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_user.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "ui/controls/custom_emoji_toast_icon.h" #include "ui/effects/animation_value.h" @@ -181,10 +182,12 @@ class PreviewAiToneExampleCard final : public Ui::RpWidget { public: PreviewAiToneExampleCard( QWidget *parent, + not_null session, std::shared_ptr> loading); void showExample(Data::AiComposeToneExample example); void showSkeleton(bool shown); + void setAnotherVisible(bool visible); [[nodiscard]] rpl::producer<> anotherExampleRequested() const; protected: @@ -192,6 +195,7 @@ protected: void paintEvent(QPaintEvent *e) override; private: + const not_null _session; const not_null _layout; const not_null _beforeTitle; const not_null _beforeBody; @@ -208,8 +212,10 @@ private: PreviewAiToneExampleCard::PreviewAiToneExampleCard( QWidget *parent, + not_null session, std::shared_ptr> loading) : RpWidget(parent) +, _session(session) , _layout(Ui::CreateChild(this)) , _beforeTitle(_layout->add( object_ptr( @@ -305,8 +311,9 @@ PreviewAiToneExampleCard::PreviewAiToneExampleCard( void PreviewAiToneExampleCard::showExample( Data::AiComposeToneExample example) { - _beforeBody->setText(example.from); - _afterBody->setText(example.to); + const auto context = Core::TextContext({ .session = _session }); + _beforeBody->setMarkedText(example.from, context); + _afterBody->setMarkedText(example.to, context); _beforeSkeleton.stop(); _afterSkeleton.stop(); if (width() > 0) { @@ -324,6 +331,10 @@ void PreviewAiToneExampleCard::showSkeleton(bool shown) { } } +void PreviewAiToneExampleCard::setAnotherVisible(bool visible) { + _another->setVisible(visible); +} + rpl::producer<> PreviewAiToneExampleCard::anotherExampleRequested() const { return _anotherExampleRequested.events(); } @@ -410,8 +421,18 @@ void PreviewAiToneBox( state->examplesCount = tone.firstExample ? 1 : 0; const auto card = body->add( - object_ptr(body, state->requesting), + object_ptr( + body, + session, + state->requesting), st::aiTonePreviewExampleCardMargin); + const auto maxExamples = session->appConfig().get( + u"aicompose_tone_examples_num"_q, + 3); + const auto updateAnother = [=] { + card->setAnotherVisible(state->examplesCount < maxExamples); + }; + updateAnother(); const auto loadAnother = [=] { if (state->requesting->current()) { return; @@ -426,6 +447,7 @@ void PreviewAiToneBox( *state->requesting = false; ++state->examplesCount; card->showExample(std::move(example)); + updateAnother(); }), crl::guard(box, [=](const MTP::Error &) { *state->requesting = false; @@ -489,10 +511,21 @@ void PreviewAiToneBox( const auto add = box->addButton( tr::lng_ai_compose_tone_preview_add(), [=] { - session->data().aiComposeTones().save(tone, false); const auto show = box->uiShow(); - box->closeBox(); - ShowToneAddedToast(show, session, tone); + session->data().aiComposeTones().save( + tone, + false, + crl::guard(box, [=] { + box->closeBox(); + ShowToneAddedToast(show, session, tone); + }), + crl::guard(box, [=](const MTP::Error &error) { + if (error.type() == u"TONES_SAVED_TOO_MANY"_q) { + ShowAiComposeToneLimitError(show, session); + } else { + box->showToast(tr::lng_ai_compose_error(tr::now)); + } + })); }); add->setFullRadius(true); } diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp index d707d22b62..11fe92a065 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_ai_compose_tones.h" #include "apiwrap.h" +#include "api/api_text_entities.h" #include "data/data_session.h" #include "main/main_session.h" @@ -72,8 +73,8 @@ AiComposeTone AiComposeTones::parseTone( if (const auto example = data.vexample_english()) { example->match([&](const MTPDaiComposeToneExample &d) { result.firstExample = AiComposeToneExample{ - .from = qs(d.vfrom()), - .to = qs(d.vto()), + .from = Api::ParseTextWithEntities(_session, d.vfrom()), + .to = Api::ParseTextWithEntities(_session, d.vto()), }; }); } @@ -176,7 +177,8 @@ void AiComposeTones::update( void AiComposeTones::save( const AiComposeTone &tone, bool unsave, - Fn done) { + Fn done, + Fn fail) { _session->api().request(MTPaicompose_SaveTone( toneToMTP(tone), unsave ? MTP_boolTrue() : MTP_boolFalse() @@ -185,7 +187,10 @@ void AiComposeTones::save( done(); } refresh(); - }).fail([=] { + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } }).send(); } @@ -260,8 +265,8 @@ void AiComposeTones::getToneExample( result.match([&](const MTPDaiComposeToneExample &data) { if (done) { done(AiComposeToneExample{ - .from = qs(data.vfrom()), - .to = qs(data.vto()), + .from = Api::ParseTextWithEntities(_session, data.vfrom()), + .to = Api::ParseTextWithEntities(_session, data.vto()), }); } }); diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.h b/Telegram/SourceFiles/data/data_ai_compose_tones.h index d4045799b5..141597760d 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.h +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/timer.h" +#include "ui/text/text_entity.h" namespace Main { class Session; @@ -20,8 +21,8 @@ class Error; namespace Data { struct AiComposeToneExample { - QString from; - QString to; + TextWithEntities from; + TextWithEntities to; }; struct AiComposeTone { @@ -65,7 +66,8 @@ public: void save( const AiComposeTone &tone, bool unsave, - Fn done = nullptr); + Fn done = nullptr, + Fn fail = nullptr); void remove( const AiComposeTone &tone, Fn done = nullptr); diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp index 66bc854f6b..4c015041b2 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.cpp +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -217,6 +217,13 @@ int PremiumLimits::botsCreatePremium() const { return appConfigLimit("bots_create_limit_premium", 40); } +int PremiumLimits::aiComposeSavedTonesDefault() const { + return appConfigLimit("aicompose_tone_saved_limit_default", 5); +} +int PremiumLimits::aiComposeSavedTonesPremium() const { + return appConfigLimit("aicompose_tone_saved_limit_premium", 20); +} + int PremiumLimits::appConfigLimit( const QString &key, int fallback) const { diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h index 92c0ad477c..dd93b5fa25 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.h +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -86,6 +86,9 @@ public: [[nodiscard]] int botsCreateDefault() const; [[nodiscard]] int botsCreatePremium() const; + [[nodiscard]] int aiComposeSavedTonesDefault() const; + [[nodiscard]] int aiComposeSavedTonesPremium() const; + private: [[nodiscard]] int appConfigLimit( const QString &key, diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index daf117be96..24720c3660 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -2162,7 +2162,7 @@ aiComposeToneDefault#9bad6414 tone:string emoji_id:long title:string = AiCompose aicompose.tonesNotModified#c1f46103 = aicompose.Tones; aicompose.tones#6c9d0efe hash:long tones:Vector users:Vector = aicompose.Tones; -aiComposeToneExample#a8dc3b99 from:string to:string = AiComposeToneExample; +aiComposeToneExample#f1d628ec from:TextWithEntities to:TextWithEntities = AiComposeToneExample; bots.accessSettings#dd1fbf93 flags:# restricted:flags.0?true add_users:flags.1?Vector = bots.AccessSettings; From eeae44810cb18c2a0454d6fdf9c656f85c7b324c Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 24 Apr 2026 12:11:23 +0700 Subject: [PATCH 102/154] Better errors handling for AI Editor. --- .../SourceFiles/boxes/create_ai_tone_box.cpp | 27 +++++-------------- .../SourceFiles/boxes/preview_ai_tone_box.cpp | 2 +- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index f378d209d7..b65b9e591f 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -601,7 +601,7 @@ void CreateAiToneBox( crl::guard(box, [=](const MTP::Error &error) { if (error.type() == u"TONES_SAVED_TOO_MANY"_q) { ShowAiComposeToneLimitError(box->uiShow(), session); - } else { + } else if (!MTP::IgnoreError(error)) { box->showToast(tr::lng_ai_compose_error(tr::now)); } })); @@ -689,26 +689,11 @@ void ShowAiComposeToneLimitError( const auto premiumLimit = limits.aiComposeSavedTonesPremium(); const auto current = premium ? premiumLimit : defaultLimit; if (premium || !premiumPossible) { - using WeakToast = base::weak_ptr; - const auto toast = std::make_shared(); - (*toast) = show->showToast({ - .text = tr::lng_ai_compose_tone_saved_limit_final( - tr::now, - lt_count, - current, - tr::rich), - .filter = crl::guard(session, [=]( - const ClickHandlerPtr &, - Qt::MouseButton button) { - if (button == Qt::LeftButton) { - if (const auto strong = toast->get()) { - strong->hideAnimated(); - (*toast) = nullptr; - } - } - return true; - }), - }); + show->showToast(tr::lng_ai_compose_tone_saved_limit_final( + tr::now, + lt_count, + current, + tr::rich)); } else { Settings::ShowPremiumPromoToast( Main::MakeSessionShow(show, session), diff --git a/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp index 74c09972af..18de2cbea8 100644 --- a/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp @@ -522,7 +522,7 @@ void PreviewAiToneBox( crl::guard(box, [=](const MTP::Error &error) { if (error.type() == u"TONES_SAVED_TOO_MANY"_q) { ShowAiComposeToneLimitError(show, session); - } else { + } else if (!MTP::IgnoreError(error)) { box->showToast(tr::lng_ai_compose_error(tr::now)); } })); From 553ecdd5f7d605b0f2a06d22fc661c1210ed9166 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 23 Apr 2026 14:16:47 +0700 Subject: [PATCH 103/154] Delete reactions from menu / list. --- Telegram/SourceFiles/api/api_report.cpp | 28 ++ Telegram/SourceFiles/api/api_report.h | 15 + Telegram/SourceFiles/api/api_who_reacted.cpp | 3 + .../boxes/moderate_messages_box.cpp | 232 +++++++++--- .../boxes/peers/edit_peer_permissions_box.cpp | 2 +- .../chat_helpers/chat_helpers.style | 8 + .../data/data_chat_participant_status.cpp | 27 +- .../view/history_view_context_menu.cpp | 86 +++-- .../reactions/history_view_reactions_list.cpp | 119 ++++++- .../reactions/history_view_reactions_list.h | 18 + .../info/profile/info_profile_actions.cpp | 87 +++-- .../controls/who_reacted_context_action.cpp | 332 +++++++++++++++++- .../ui/controls/who_reacted_context_action.h | 28 +- 13 files changed, 855 insertions(+), 130 deletions(-) diff --git a/Telegram/SourceFiles/api/api_report.cpp b/Telegram/SourceFiles/api/api_report.cpp index 05b6d93591..62f2458d6c 100644 --- a/Telegram/SourceFiles/api/api_report.cpp +++ b/Telegram/SourceFiles/api/api_report.cpp @@ -144,6 +144,34 @@ auto CreateReportMessagesOrStoriesCallback( }; } +ReactionReportCapabilities GetReactionReportCapabilities( + not_null group, + not_null participant) { + const auto channel = group->asMegagroup(); + return channel + ? ReactionReportCapabilities{ + .canReport = channel->isPublic() && !participant->isSelf(), + .canBan = channel->canRestrictParticipant(participant), + } + : ReactionReportCapabilities(); +} + +void ReportReaction( + std::shared_ptr show, + not_null group, + MsgId messageId, + not_null participant) { + group->session().api().request(MTPmessages_ReportReaction( + group->input(), + MTP_int(messageId.bare), + participant->input() + )).done([=] { + if (show) { + show->showToast(tr::lng_report_thanks(tr::now)); + } + }).send(); +} + void ReportSpam( not_null sender, const MessageIdsList &ids) { diff --git a/Telegram/SourceFiles/api/api_report.h b/Telegram/SourceFiles/api/api_report.h index e503102ec5..0b35c408de 100644 --- a/Telegram/SourceFiles/api/api_report.h +++ b/Telegram/SourceFiles/api/api_report.h @@ -53,6 +53,21 @@ void SendPhotoReport( not_null peer) -> Fn)>; +struct ReactionReportCapabilities final { + bool canReport = false; + bool canBan = false; +}; + +[[nodiscard]] ReactionReportCapabilities GetReactionReportCapabilities( + not_null group, + not_null participant); + +void ReportReaction( + std::shared_ptr show, + not_null group, + MsgId messageId, + not_null participant); + void ReportSpam( not_null sender, const MessageIdsList &ids); diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index f3af5312fb..ea4a3cd4f6 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -512,11 +512,13 @@ void RegenerateParticipants(not_null state, int small, int large) { const auto peer = userpic.peer; const auto date = userpic.date; const auto id = peer->id.value; + const auto self = peer->isSelf(); const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id); if (was != end(old)) { was->name = peer->name(); was->date = FormatReadDate(date, currentDate); was->dateReacted = userpic.dateReacted; + was->self = self; now.push_back(std::move(*was)); continue; } @@ -524,6 +526,7 @@ void RegenerateParticipants(not_null state, int small, int large) { .name = peer->name(), .date = FormatReadDate(date, currentDate), .dateReacted = userpic.dateReacted, + .self = self, .customEntityData = userpic.customEntityData, .userpicLarge = GenerateUserpic(userpic, large), .userpicKey = userpic.uniqueKey, diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 0685274b33..02aa6a8eee 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -82,22 +82,43 @@ const char kModerateCommonGroups[] = "moderate-common-groups"; namespace { struct ModerateOptions final { - bool allCanBan = false; - bool allCanDelete = false; + bool reportSpam = false; + bool deleteAllMessages = false; + bool deleteAllReactions = false; + bool banOrRestrict = false; Participants participants; }; +[[nodiscard]] bool PeerCanDeleteMessages(not_null peer) { + if (const auto chat = peer->asChat()) { + return chat->canDeleteMessages(); + } + const auto channel = peer->asChannel(); + return channel && channel->canDeleteMessages(); +} + +[[nodiscard]] bool IsExcludedModerateParticipant( + not_null peer, + not_null participant) { + if ((participant == peer) || participant->isSelf()) { + return true; + } else if (const auto channel = participant->asChannel()) { + return (channel->discussionLink() == peer); + } + return false; +} + ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) { Expects(!items.empty()); auto result = ModerateOptions{ - .allCanBan = true, - .allCanDelete = true, + .deleteAllMessages = true, + .banOrRestrict = true, }; const auto peer = items.front()->history()->peer; for (const auto &item : items) { - if (!result.allCanBan && !result.allCanDelete) { + if (!result.deleteAllMessages && !result.banOrRestrict) { return {}; } if (peer != item->history()->peer) { @@ -114,10 +135,10 @@ ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) { } } if (!item->suggestBanReport()) { - result.allCanBan = false; + result.banOrRestrict = false; } if (!item->suggestDeleteAllReport()) { - result.allCanDelete = false; + result.deleteAllMessages = false; } if (const auto p = item->from()) { if (!ranges::contains(result.participants, not_null{ p })) { @@ -125,9 +146,38 @@ ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) { } } } + result.deleteAllReactions = result.deleteAllMessages; + result.reportSpam = result.deleteAllMessages || result.banOrRestrict; return result; } +ModerateOptions CalculateModerateOptions(const ModerateReactionEntry &reaction) { + auto result = ModerateOptions{ + .participants = { reaction.participant }, + }; + if (IsExcludedModerateParticipant(reaction.peer, reaction.participant)) { + return result; + } + result.reportSpam = Api::GetReactionReportCapabilities( + reaction.peer, + reaction.participant + ).canReport || (reaction.peer->asChannel() != nullptr); + result.deleteAllReactions = PeerCanDeleteMessages(reaction.peer); + if (const auto channel = reaction.peer->asChannel()) { + result.deleteAllMessages = channel->canDeleteMessages(); + result.banOrRestrict = channel->canRestrictParticipant( + reaction.participant); + } + return result; +} + +[[nodiscard]] bool HasModerateActions(const ModerateOptions &options) { + return options.reportSpam + || options.deleteAllMessages + || options.deleteAllReactions + || options.banOrRestrict; +} + [[nodiscard]] rpl::producer> MessagesCountValue( not_null history, std::vector> from) { @@ -310,7 +360,7 @@ void ProccessCommonGroups( Fn)> processHas) { const auto moderateOptions = CalculateModerateOptions(items); if (moderateOptions.participants.size() != 1 - || !moderateOptions.allCanBan) { + || !moderateOptions.banOrRestrict) { return; } const auto participant = moderateOptions.participants.front(); @@ -364,13 +414,11 @@ void CreateModerateMessagesBox( const auto moderateOptions = hasItems ? CalculateModerateOptions(items) - : ModerateOptions{ - .allCanBan = false, - .allCanDelete = false, - .participants = { reaction->participant }, - }; - const auto allCanBan = moderateOptions.allCanBan; - const auto allCanDelete = moderateOptions.allCanDelete; + : CalculateModerateOptions(*reaction); + const auto reportSpam = moderateOptions.reportSpam; + const auto deleteAllMessages = moderateOptions.deleteAllMessages; + const auto deleteAllReactions = moderateOptions.deleteAllReactions; + const auto banOrRestrict = moderateOptions.banOrRestrict; const auto &participants = moderateOptions.participants; const auto inner = box->verticalLayout(); @@ -393,31 +441,36 @@ void CreateModerateMessagesBox( const auto session = hasItems ? &firstItem->history()->session() : &reaction->peer->session(); + const auto peer = hasItems + ? firstItem->history()->peer + : reaction->peer; const auto history = hasItems ? firstItem->history().get() - : session->data().historyLoaded(reaction->peer); - const auto historyPeerId = hasItems - ? history->peer->id - : reaction->peer->id; + : session->data().historyLoaded(peer); + const auto historyPeerId = peer->id; const auto ids = hasItems ? session->data().itemsToIds(items) - : MessageIdsList(); + : MessageIdsList{ FullMsgId(reaction->peer->id, reaction->msgId) }; const auto selectedMessagesByParticipant = [&] { auto result = base::flat_map(); - if (!hasItems) { + if (!hasItems && !hasReaction) { return result; } - for (const auto &item : items) { - const auto from = item->from(); - if (!from) { - continue; - } - const auto i = result.find(from->id); - if (i == result.end()) { - result.emplace(from->id, 1); - } else { - ++i->second; + if (hasItems) { + for (const auto &item : items) { + const auto from = item->from(); + if (!from) { + continue; + } + const auto i = result.find(from->id); + if (i == result.end()) { + result.emplace(from->id, 1); + } else { + ++i->second; + } } + } else { + result.emplace(reaction->participant->id, 1); } return result; }(); @@ -654,7 +707,7 @@ void CreateModerateMessagesBox( subtitle->entity()->setTextColorOverride(st::windowSubTextFg->c); subtitle->hide(anim::type::instant); Ui::AddSkip(inner); - if (hasItems) { + if (reportSpam) { const auto report = box->addRow( object_ptr( box, @@ -670,13 +723,24 @@ void CreateModerateMessagesBox( handleConfirmation(report, controller, [=]( not_null p, not_null c) { - Api::ReportSpam(p, ids); + if (reaction.has_value() + && Api::GetReactionReportCapabilities( + reaction->peer, + p + ).canReport) { + Api::ReportReaction( + box->uiShow(), + reaction->peer, + reaction->msgId, + p); + } else { + Api::ReportSpam(p, ids); + } }); } - const auto showMessagesCheckbox = allCanDelete && hasItems; - const auto showReactionsCheckbox = (allCanDelete && hasItems) - || (hasReaction && !hasItems); + const auto showMessagesCheckbox = deleteAllMessages; + const auto showReactionsCheckbox = deleteAllReactions; if (showMessagesCheckbox || showReactionsCheckbox) { Ui::AddSkip(inner); Ui::AddSkip(inner); @@ -685,6 +749,7 @@ void CreateModerateMessagesBox( : std::vector(); if (showMessagesCheckbox) { + Assert(history != nullptr); deleteMessagesCounts = box->lifetime().make_state< rpl::variable>>( base::flat_map()); @@ -751,9 +816,6 @@ void CreateModerateMessagesBox( && !effectiveCheckedParticipants( deleteReactions, deleteReactionsController).empty()) { - const auto peer = hasItems - ? history->peer.get() - : reaction->peer.get(); for (const auto &participant : deleteReactionsController->collectRequests()) { peer->session().api() @@ -779,6 +841,7 @@ void CreateModerateMessagesBox( int count = 0; bool resolved = false; }; + const auto baseMessagesCount = int(ids.size()); const auto langUpdated = rpl::single( 0 ) | rpl::then(Lang::Updated() | rpl::map([] { @@ -788,7 +851,7 @@ void CreateModerateMessagesBox( const base::flat_map &messagesCounts, const Participants &checked) { auto result = MessageTitleData{ - .count = itemsCount, + .count = baseMessagesCount, .resolved = true, }; for (const auto &peer : checked) { @@ -806,7 +869,7 @@ void CreateModerateMessagesBox( return result; }; auto title = [&]() -> rpl::producer { - if (hasItems && showMessagesCheckbox) { + if (showMessagesCheckbox && !(hasReaction && !hasItems)) { auto messageTitleData = rpl::combine( deleteMessagesCounts->value(), checkedParticipantsValue( @@ -837,6 +900,51 @@ void CreateModerateMessagesBox( makeTitleLoadingDescriptor())) .append(text.mid(zeroIndex + 1)); }); + } else if (hasReaction && showMessagesCheckbox) { + auto messageTitleData = rpl::combine( + deleteMessagesCounts->value(), + checkedParticipantsValue( + not_null{ deleteMessages }, + not_null{ deleteMessagesController }) + ) | rpl::map(makeMessageTitleData); + auto deleteReactionsChecked = deleteReactions + ? deleteReactions->checkedValue() + : rpl::single(false); + return rpl::combine( + deleteMessages->checkedValue(), + std::move(messageTitleData), + std::move(deleteReactionsChecked), + rpl::duplicate(langUpdated) + ) | rpl::map([=]( + bool deleteMessagesChecked, + const MessageTitleData &data, + bool deleteReactionsChecked, + int) { + if (!deleteMessagesChecked) { + return TextWithEntities{ deleteReactionsChecked + ? tr::lng_delete_title_reaction_all(tr::now) + : tr::lng_delete_title_reaction_this(tr::now) }; + } + const auto count = data.count; + const auto resolved = data.resolved; + const auto text = (count == 1) + ? tr::lng_delete_title_message_one(tr::now) + : tr::lng_delete_title_message_many( + tr::now, + lt_count, + count); + if (resolved || count != 0) { + return TextWithEntities{ text }; + } + const auto zeroIndex = text.indexOf('0'); + return (zeroIndex == -1) + ? TextWithEntities{ text } + : TextWithEntities() + .append(text.mid(0, zeroIndex)) + .append(Ui::Text::LottieEmoji( + makeTitleLoadingDescriptor())) + .append(text.mid(zeroIndex + 1)); + }); } else if (hasItems) { return rpl::duplicate(langUpdated) | rpl::map([=](int) { return (itemsCount == 1) @@ -888,7 +996,7 @@ void CreateModerateMessagesBox( SomeReactions, AllReactions, }; - if (hasItems) { + if (hasItems || (hasReaction && showMessagesCheckbox)) { const auto subtitleKind = box->lifetime().make_state< rpl::variable>(SubtitleKind::None); auto reactionsCheckedValue = showReactionsCheckbox @@ -896,6 +1004,13 @@ void CreateModerateMessagesBox( not_null{ deleteReactions }, not_null{ deleteReactionsController }) : rpl::single(Participants()); + auto messageTitleShownValue = [&] { + return hasItems + ? rpl::single(true) + : (hasReaction && showMessagesCheckbox) + ? deleteMessages->checkedValue() + : rpl::single(false); + }(); rpl::combine( subtitleKind->value(), rpl::duplicate(langUpdated) @@ -916,22 +1031,25 @@ void CreateModerateMessagesBox( }, subtitle->lifetime()); rpl::combine( std::move(reactionsCheckedValue), - rpl::single(hasReaction) - ) | rpl::on_next([=](const Participants &checked, bool hasReaction) { + std::move(messageTitleShownValue) + ) | rpl::on_next([=]( + const Participants &checked, + bool messageTitleShown) { auto kind = SubtitleKind::None; - if (!checked.empty()) { - kind = (checked.size() == participants.size()) - ? SubtitleKind::AllReactions - : SubtitleKind::SomeReactions; - } else if (hasReaction) { - kind = SubtitleKind::ThisReaction; + if (messageTitleShown) { + if (!checked.empty()) { + kind = (checked.size() == participants.size()) + ? SubtitleKind::AllReactions + : SubtitleKind::SomeReactions; + } else if (hasReaction) { + kind = SubtitleKind::ThisReaction; + } } subtitleKind->force_assign(kind); subtitle->toggle(kind != SubtitleKind::None, anim::type::normal); }, subtitle->lifetime()); } - if (hasItems && allCanBan) { - const auto peer = items.front()->history()->peer; + if (banOrRestrict) { auto ownedWrap = peer->isMonoforum() ? nullptr : object_ptr>( @@ -1137,7 +1255,13 @@ void CreateModerateMessagesBox( session->data().histories().deleteMessages(ids, true); session->data().sendHistoryChangeNotifications(); } - if (reaction) { + const auto deleteThisReaction = reaction + && !ranges::contains( + effectiveCheckedParticipants( + deleteReactions, + deleteReactionsController), + reaction->participant); + if (deleteThisReaction) { session->api().deleteParticipantReaction( reaction->peer, reaction->msgId, @@ -1150,7 +1274,7 @@ void CreateModerateMessagesBox( bool CanCreateModerateMessagesBox(const HistoryItemsList &items) { const auto options = CalculateModerateOptions(items); - return (options.allCanBan || options.allCanDelete) + return HasModerateActions(options) && !options.participants.empty(); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 3a89affff2..78eb5c2435 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -90,7 +90,6 @@ constexpr auto kDefaultChargeStars = 10; | Flag::SendGames | Flag::SendInline, tr::lng_rights_chat_stickers(tr::now) }, { Flag::EmbedLinks, tr::lng_rights_chat_send_links(tr::now) }, - { Flag::SendReactions, tr::lng_rights_chat_send_reactions(tr::now) }, { Flag::SendPolls, tr::lng_rights_chat_send_polls(tr::now) }, }; auto second = std::vector{ @@ -101,6 +100,7 @@ constexpr auto kDefaultChargeStars = 10; ? tr::lng_rights_group_edit_rank_single : tr::lng_rights_group_edit_rank)(tr::now) }, { Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) }, + { Flag::SendReactions, tr::lng_rights_chat_send_reactions(tr::now) }, }; if (!options.isForum) { second.erase( diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 92daf29f68..4568df08a2 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -609,6 +609,14 @@ stickerPanRemoveSet: IconButton(hashtagClose) { iconPosition: point(-1px, -1px); rippleAreaPosition: point(0px, 0px); } +whoReadClose: IconButton(stickerPanRemoveSet) { + width: 20px; + height: 20px; + icon: smallCloseIconOver; + rippleAreaSize: 20px; +} +whoReadCloseVisibleRadius: 7px; +whoReadCloseBlurPadding: 5px; stickerIconMove: 400; stickerPreviewDuration: 150; stickerPreviewMin: 0.1; diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index d63c0acade..79c1fa0047 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -249,12 +249,9 @@ bool CanSendAnyOf( if (!chat->amIn()) { return false; } - for (const auto right : AllSendRestrictionsList()) { - if ((rights & right) && !chat->amRestricted(right)) { - return true; - } - } - return false; + return chat->amCreator() + || chat->hasAdminRights() + || (rights & ~chat->defaultRestrictions()); } else if (const auto channel = peer->asChannel()) { if (channel->monoforumDisabled()) { return false; @@ -266,17 +263,15 @@ bool CanSendAnyOf( || channel->isMonoforum(); if (!allowed || (forbidInForums && channel->isForum())) { return false; - } else if (channel->canPostMessages()) { - return true; - } else if (channel->isBroadcast()) { - return false; } - for (const auto right : AllSendRestrictionsList()) { - if ((rights & right) && !channel->amRestricted(right)) { - return true; - } - } - return false; + const auto restricted = channel->restrictions() + | (channel->unrestrictedByBoosts() + ? ChatRestrictions() + : channel->defaultRestrictions()); + return channel->canPostMessages() + || (!channel->isBroadcast() + && (channel->hasAdminRights() + || (rights & ~restricted))); } Unexpected("Peer type in CanSendAnyOf."); } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index bc77c7bac7..df17b091ad 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -32,7 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions_list.h" #include "info/info_memento.h" -#include "info/profile/info_profile_widget.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_action.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" @@ -1172,26 +1171,36 @@ void EditTagBox( }); } -void ShowWhoReadInfo( +[[nodiscard]] Fn MakeModerateReactionChosen( not_null controller, FullMsgId itemId, - Ui::WhoReadParticipant who) { - const auto peer = controller->session().data().peer(itemId.peer); - const auto participant = peer->owner().peer(PeerId(who.id)); - const auto migrated = participant->migrateFrom(); - const auto origin = who.dateReacted - ? Info::Profile::Origin{ - Info::Profile::GroupReactionOrigin{ peer, itemId.msg }, + not_null peer, + Fn hideMenu) { + if (!Reactions::CanModerateReactionByDeleteMessages(peer)) { + return {}; + } + return [=, hideMenu = std::move(hideMenu)](Ui::WhoReadParticipant who) { + if (who.id == 0 || who.customEntityData.isEmpty()) { + return; } - : Info::Profile::Origin(); - auto memento = std::make_shared( - std::vector>{ - std::make_shared( - participant, - migrated ? migrated->id : PeerId(), - origin), - }); - controller->showSection(std::move(memento)); + const auto item = controller->session().data().message(itemId); + if (!item) { + return; + } + const auto participant = item->history()->peer->owner().peer( + PeerId(who.id)); + if (participant->isSelf()) { + return; + } + if (hideMenu) { + hideMenu(); + } + Reactions::ShowModerateReactionBox( + controller, + item->history()->peer, + itemId.msg, + participant); + }; } [[nodiscard]] rpl::producer> LookupMessageAuthor( @@ -2078,8 +2087,23 @@ void AddWhoReactedAction( if (const auto strong = weak.get()) { strong->hideMenu(); } - ShowWhoReadInfo(controller, itemId, who); + const auto participant = user->owner().peer(PeerId(who.id)); + Reactions::ShowReactionParticipantInfo( + controller, + participant, + user, + itemId.msg, + who.dateReacted); }; + const auto moderateReactionChosen = MakeModerateReactionChosen( + controller, + itemId, + user, + [=] { + if (const auto strong = weak.get()) { + strong->hideMenu(); + } + }); const auto showAllChosen = [=, itemId = item->fullId()]{ // Pressing on an item that has a submenu doesn't hide it :( if (const auto strong = weak.get()) { @@ -2114,7 +2138,8 @@ void AddWhoReactedAction( Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds), Data::ReactedMenuFactory(&controller->session()), participantChosen, - showAllChosen)); + showAllChosen, + moderateReactionChosen)); AddWhenEditedForwardedAuthorActionHelper( menu, item, @@ -2269,8 +2294,24 @@ void ShowWhoReactedMenu( }; const auto itemId = item->fullId(); const auto participantChosen = [=](Ui::WhoReadParticipant who) { - ShowWhoReadInfo(controller, itemId, who); + const auto originPeer = item->history()->peer; + const auto participant = originPeer->owner().peer(PeerId(who.id)); + Reactions::ShowReactionParticipantInfo( + controller, + participant, + originPeer, + itemId.msg, + who.dateReacted); }; + const auto moderateReactionChosen = MakeModerateReactionChosen( + controller, + itemId, + item->history()->peer, + [=] { + if (*menu) { + (*menu)->hideMenu(); + } + }); const auto showAllChosen = [=, itemId = item->fullId()]{ if (const auto item = controller->session().data().message(itemId)) { controller->showSection(std::make_shared( @@ -2290,7 +2331,8 @@ void ShowWhoReactedMenu( const auto filler = lifetime.make_state( Data::ReactedMenuFactory(&controller->session()), participantChosen, - showAllChosen); + showAllChosen, + moderateReactionChosen); const auto state = lifetime.make_state(); Api::WhoReacted( item, diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp index c48b0ed6cd..5605a11f74 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp @@ -8,21 +8,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/reactions/history_view_reactions_list.h" #include "history/view/reactions/history_view_reactions_tabs.h" +#include "boxes/moderate_messages_box.h" #include "boxes/peer_list_box.h" #include "boxes/peers/prepare_short_info_box.h" +#include "info/info_memento.h" +#include "info/profile/info_profile_widget.h" #include "window/window_session_controller.h" #include "history/history_item.h" #include "history/history.h" #include "api/api_who_reacted.h" #include "ui/controls/who_reacted_context_action.h" +#include "ui/layers/generic_box.h" #include "ui/text/text_custom_emoji.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/popup_menu.h" #include "ui/painter.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_message_reaction_id.h" #include "main/main_session.h" #include "data/data_session.h" #include "data/data_peer.h" +#include "data/data_chat.h" +#include "data/data_channel.h" #include "lang/lang_keys.h" +#include "styles/style_boxes.h" +#include "styles/style_menu_icons.h" namespace HistoryView::Reactions { namespace { @@ -38,10 +49,14 @@ public: uint64 id, not_null peer, const Ui::Text::CustomEmojiFactory &factory, + ReactionId reaction, QStringView reactionEntityData, Fn repaint, Fn paused); + [[nodiscard]] const ReactionId &reaction() const; + [[nodiscard]] bool isReactionRow() const; + QSize rightActionSize() const override; QMargins rightActionMargins() const override; bool rightActionDisabled() const override; @@ -54,6 +69,7 @@ public: bool actionSelected) override; private: + ReactionId _reaction; std::unique_ptr _custom; Fn _paused; @@ -71,6 +87,9 @@ public: Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; void loadMoreRows() override; std::unique_ptr createRestoredRow( @@ -148,16 +167,26 @@ Row::Row( uint64 id, not_null peer, const Ui::Text::CustomEmojiFactory &factory, + ReactionId reaction, QStringView reactionEntityData, Fn repaint, Fn paused) : PeerListRow(peer, id) +, _reaction(std::move(reaction)) , _custom(reactionEntityData.isEmpty() ? nullptr : factory(reactionEntityData, { .repaint = [=] { repaint(this); } })) , _paused(std::move(paused)) { } +const ReactionId &Row::reaction() const { + return _reaction; +} + +bool Row::isReactionRow() const { + return !_reaction.empty(); +} + QSize Row::rightActionSize() const { const auto size = Ui::Emoji::GetSizeNormal() / style::DevicePixelRatio(); return _custom ? QSize(size, size) : QSize(); @@ -413,11 +442,48 @@ void Controller::loadMore(const ReactionId &reaction) { void Controller::rowClicked(not_null row) { const auto window = _window; const auto peer = row->peer(); + const auto originPeer = _peer; + const auto originMsgId = _itemId.msg; + const auto reactionRow = static_cast(row.get())->isReactionRow(); crl::on_main(window, [=] { - window->showPeerInfo(peer); + ShowReactionParticipantInfo( + window, + peer, + originPeer, + originMsgId, + reactionRow); }); } +base::unique_qptr Controller::rowContextMenu( + QWidget *parent, + not_null row) { + const auto reactionRow = static_cast(row.get()); + const auto participant = row->peer(); + if (!reactionRow->isReactionRow() + || participant->isSelf() + || !CanModerateReactionByDeleteMessages(_peer)) { + return nullptr; + } + + auto result = base::make_unique_q( + parent, + st::popupMenuWithIcons); + Ui::Menu::CreateAddActionCallback(result.get())({ + .text = tr::lng_context_delete_this_reaction(tr::now), + .handler = [=] { + ShowModerateReactionBox( + _window->parentController(), + _peer, + _itemId.msg, + participant); + }, + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); + return result; +} + bool Controller::appendRow(not_null peer, ReactionId reaction) { if (delegate()->peerListFindRow(id(peer, reaction))) { return false; @@ -433,6 +499,7 @@ std::unique_ptr Controller::createRow( id(peer, reaction), peer, _factory, + reaction, Data::ReactionEntityData(reaction), [=](Row *row) { delegate()->peerListUpdateRow(row); }, [=] { return _window->parentController()->isGifPausedAtLeastFor( @@ -441,6 +508,56 @@ std::unique_ptr Controller::createRow( } // namespace +bool CanModerateReactionByDeleteMessages(not_null originPeer) { + if (const auto chat = originPeer->asChat()) { + return chat->canDeleteMessages(); + } else if (const auto channel = originPeer->asChannel()) { + return channel->canDeleteMessages(); + } + return false; +} + +void ShowModerateReactionBox( + not_null controller, + not_null originPeer, + MsgId originMsgId, + not_null participant) { + controller->show(Box( + CreateModerateMessagesBox, + ModerateMessagesBoxEntry{ + .reaction = ModerateReactionEntry{ + .peer = originPeer, + .msgId = originMsgId, + .participant = participant, + }, + }, + nullptr, + DefaultModerateMessagesBoxOptions())); +} + +void ShowReactionParticipantInfo( + not_null window, + not_null participant, + not_null originPeer, + MsgId originMsgId, + bool reactionRow) { + if (!reactionRow) { + window->showPeerInfo(participant); + return; + } + const auto migrated = participant->migrateFrom(); + auto memento = std::make_shared( + std::vector>{ + std::make_shared( + participant, + migrated ? migrated->id : PeerId(), + Info::Profile::Origin{ + Info::Profile::GroupReactionOrigin{ originPeer, originMsgId }, + }), + }); + window->showSection(std::move(memento)); +} + Data::ReactionId DefaultSelectedTab( not_null item, std::shared_ptr whoReadIds) { diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h index be6dc59fec..745915bc32 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" class HistoryItem; +class PeerData; class PeerListController; namespace Data { @@ -52,6 +53,23 @@ struct PreparedFullList { std::unique_ptr controller; Fn switchTab; }; + +[[nodiscard]] bool CanModerateReactionByDeleteMessages( + not_null originPeer); + +void ShowModerateReactionBox( + not_null controller, + not_null originPeer, + MsgId originMsgId, + not_null participant); + +void ShowReactionParticipantInfo( + not_null window, + not_null participant, + not_null originPeer, + MsgId originMsgId, + bool reactionRow); + [[nodiscard]] PreparedFullList FullListController( not_null window, FullMsgId itemId, diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 8cdc5c2f14..41da53404d 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_blocked_peers.h" #include "api/api_chat_participants.h" #include "api/api_credits.h" +#include "api/api_report.h" #include "api/api_statistics.h" #include "apiwrap.h" #include "base/call_delayed.h" @@ -51,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "history/history_item_helpers.h" #include "history/view/history_view_item_preview.h" +#include "history/view/reactions/history_view_reactions_list.h" #include "info/bot/earn/info_bot_earn_widget.h" #include "info/bot/starref/info_bot_starref_common.h" #include "info/channel_statistics/earn/earn_format.h" @@ -1235,6 +1237,10 @@ private: void addReportReaction( Ui::MultiSlideTracker &tracker, Ui::MultiSlideTracker *buttonTracker); + void addDeleteReaction( + GroupReactionOrigin data, + Ui::MultiSlideTracker &tracker, + Ui::MultiSlideTracker *buttonTracker); void addReportReaction( GroupReactionOrigin data, bool ban, @@ -1332,13 +1338,11 @@ void ReportReactionBox( ChatRestrictionsInfo()); } } - data.group->session().api().request(MTPmessages_ReportReaction( - data.group->input(), - MTP_int(data.messageId.bare), - participant->input() - )).done(crl::guard(controller, [=] { - controller->showToast(tr::lng_report_thanks(tr::now)); - })).send(); + Api::ReportReaction( + controller->uiShow(), + data.group, + data.messageId, + participant); sent(); box->closeBox(); }, st::attentionBoxButton); @@ -2211,27 +2215,64 @@ void DetailsFiller::addReportReaction( Ui::MultiSlideTracker &tracker, Ui::MultiSlideTracker *buttonTracker) { v::match(_origin.data, [&](GroupReactionOrigin data) { - const auto user = _peer->asUser(); if (_peer->isSelf()) { return; -#if 0 // Only public groups allow reaction reports for now. - } else if (const auto chat = data.group->asChat()) { - const auto ban = chat->canBanMembers() - && (!user || !chat->admins.contains(_peer)) - && (!user || chat->creator != user->id); - addReportReaction(data, ban, tracker); -#endif - } else if (const auto channel = data.group->asMegagroup()) { - if (channel->isPublic()) { - const auto ban = channel->canBanMembers() - && (!user || !channel->mgInfo->admins.contains(user->id)) - && (!user || channel->mgInfo->creator != user); - addReportReaction(data, ban, tracker, buttonTracker); - } + } + if (HistoryView::Reactions::CanModerateReactionByDeleteMessages( + data.group)) { + addDeleteReaction(data, tracker, buttonTracker); + return; + } + const auto capabilities = Api::GetReactionReportCapabilities( + data.group, + _peer); + if (capabilities.canReport) { + addReportReaction( + data, + capabilities.canBan, + tracker, + buttonTracker); } }, [](const auto &) {}); } +void DetailsFiller::addDeleteReaction( + GroupReactionOrigin data, + Ui::MultiSlideTracker &tracker, + Ui::MultiSlideTracker *buttonTracker) { + const auto peer = _peer; + if (!peer) { + return; + } + const auto controller = _controller->parentController(); + const auto wrap = _wrap->add( + object_ptr>( + _wrap.data(), + object_ptr(_wrap.data()))); + Ui::AddSkip(wrap->entity()); + auto shown = rpl::single(true); + wrap->toggleOn(rpl::duplicate(shown)); + rpl::duplicate(shown) | rpl::on_next([=](bool shown) { + if (shown) { + _dividerOverridden.force_assign(false); + } + }, wrap->lifetime()); + AddMainButton( + _wrap, + tr::lng_context_delete_this_reaction(), + std::move(shown), + [=] { + HistoryView::Reactions::ShowModerateReactionBox( + controller, + data.group, + data.messageId, + peer); + }, + tracker, + buttonTracker, + st::infoMainButtonAttention); +} + void DetailsFiller::addReportReaction( GroupReactionOrigin data, bool ban, @@ -2422,7 +2463,7 @@ object_ptr DetailsFiller::fill() { } } } - if (!user->isSelf() && !_sublist) { + if (!_sublist) { addReportReaction(_mainTracker, &lastButtonTracker); } } else if (const auto channel = _peer->asChannel()) { diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index c0aa82fd18..08062aa21a 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -12,11 +12,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "ui/effects/ripple_animation.h" #include "ui/chat/group_call_userpics.h" +#include "ui/image/image_prepare.h" +#include "ui/round_rect.h" #include "ui/text/text_custom_emoji.h" #include "ui/emoji_config.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" +#include "styles/style_basic.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" @@ -85,7 +88,8 @@ public: rpl::producer content, CustomEmojiFactory factory, Fn participantChosen, - Fn showAllChosen); + Fn showAllChosen, + Fn moderateReactionChosen); bool isEnabled() const override; not_null action() const override; @@ -111,6 +115,7 @@ private: const not_null _dummyAction; const Fn _participantChosen; const Fn _showAllChosen; + const Fn _moderateReactionChosen; const std::unique_ptr _userpics; const style::Menu &_st; const CustomEmojiFactory _customEmojiFactory; @@ -174,6 +179,83 @@ TextParseOptions MenuTextOptions = { Qt::LayoutDirectionAuto, // dir }; +struct CloseBadgeCache { + QImage badge; + QImage mask; +}; + +[[nodiscard]] QPainterPath WhoReactedCloseBadgePath(const QRect &rect) { + return Ui::ComplexRoundedRectPath( + rect, + 0, + 0, + st::whoReadCloseVisibleRadius, + 0); +} + +[[nodiscard]] QPoint WhoReactedCloseIconPosition( + const QRect &rect, + const style::icon &icon) { + auto position = st::whoReadClose.iconPosition; + if (position.x() < 0) { + position.setX((rect.width() - icon.width()) / 2); + } + if (position.y() < 0) { + position.setY((rect.height() - icon.height()) / 2); + } + return rect.topLeft() + position; +} + +[[nodiscard]] CloseBadgeCache GenerateWhoReactedCloseBadgeCache( + QSize closeSize, + const style::color &fill, + const style::color &shadowColor) { + if (closeSize.isEmpty()) { + return {}; + } + const auto blur = st::whoReadCloseBlurPadding; + const auto ratio = style::DevicePixelRatio(); + const auto badgeRect = QRect(QPoint(blur, blur), closeSize); + const auto maskRect = QRect(QPoint(), closeSize); + const auto outer = badgeRect.marginsAdded(QMargins(blur, blur, blur, blur)); + + auto badge = QImage( + outer.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + badge.setDevicePixelRatio(ratio); + badge.fill(Qt::transparent); + { + Painter p(&badge); + auto hq = PainterHighQualityEnabler(p); + auto shadow = shadowColor->c; + shadow.setAlphaF(shadow.alphaF() * 0.18); + p.setPen(Qt::NoPen); + p.setBrush(shadow); + p.drawPath(WhoReactedCloseBadgePath(badgeRect)); + } + badge = Images::Blur(std::move(badge), true); + badge.setDevicePixelRatio(ratio); + { + Painter p(&badge); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(fill); + p.drawPath(WhoReactedCloseBadgePath(badgeRect)); + } + auto mask = Ui::RippleAnimation::MaskByDrawer( + closeSize, + false, + [&](QPainter &p) { + p.setPen(Qt::NoPen); + p.setBrush(Qt::white); + p.drawPath(WhoReactedCloseBadgePath(maskRect)); + }); + return { + .badge = std::move(badge), + .mask = std::move(mask), + }; +} + [[nodiscard]] QString FormatReactedString(int reacted, int seen) { const auto projection = [&](const QString &text) { return Lang::StringWithReacted{ text, seen }; @@ -198,19 +280,25 @@ Action::Action( rpl::producer content, Text::CustomEmojiFactory factory, Fn participantChosen, - Fn showAllChosen) + Fn showAllChosen, + Fn moderateReactionChosen) : ItemBase(parentMenu->menu(), parentMenu->menu()->st()) , _parentMenu(parentMenu) , _dummyAction(CreateChild(parentMenu->menu().get())) , _participantChosen(std::move(participantChosen)) , _showAllChosen(std::move(showAllChosen)) +, _moderateReactionChosen(std::move(moderateReactionChosen)) , _userpics(std::make_unique( st::defaultWhoRead.userpics, rpl::never(), [=] { update(); })) , _st(parentMenu->menu()->st()) , _customEmojiFactory(std::move(factory)) -, _submenu(_customEmojiFactory, _participantChosen, _showAllChosen) +, _submenu( + _customEmojiFactory, + _participantChosen, + _showAllChosen, + _moderateReactionChosen) , _height(st::defaultWhoRead.itemPadding.top() + _st.itemStyle.font->height + st::defaultWhoRead.itemPadding.bottom()) { @@ -758,6 +846,31 @@ WhoReactedEntryAction::WhoReactedEntryAction( }, lifetime()); enableMouseSelecting(); + selects( + ) | rpl::on_next([=](const auto &) { + refreshCloseMouseTracking(); + updateCloseHovered(QCursor::pos()); + }, lifetime()); + + style::PaletteChanged() | rpl::on_next([=] { + invalidateCloseCache(); + update(); + }, lifetime()); + + events() | rpl::on_next([=](not_null e) { + if (e->type() != QEvent::Leave) { + return; + } + if (!_closeHovered && !_closePressed) { + return; + } + _closeHovered = false; + _closePressed = false; + _closeRippleActive = false; + finishAnimating(); + invalidateCloseCache(); + update(); + }, lifetime()); } not_null WhoReactedEntryAction::action() const { @@ -772,8 +885,162 @@ int WhoReactedEntryAction::contentHeight() const { return _height; } +void WhoReactedEntryAction::mousePressEvent(QMouseEvent *e) { + updateCloseHovered(e->globalPos()); + const auto menu = static_cast(parentWidget()); + if (!menu->hasMouseMoved(e->globalPos())) { + return; + } + const auto closePressed = closeAffordanceActive() + && (e->button() == Qt::LeftButton) + && _closeRect.contains(e->pos()); + if (!closePressed) { + _closePressed = false; + _closeRippleActive = false; + ItemBase::mousePressEvent(e); + return; + } + if (!_closeHovered) { + _closeHovered = true; + finishAnimating(); + invalidateCloseCache(); + update(); + } + _closePressed = true; + _closeRippleActive = true; + RippleButton::mousePressEvent(e); +} + +void WhoReactedEntryAction::mouseMoveEvent(QMouseEvent *e) { + if (_closePressed) { + const auto menu = static_cast(parentWidget()); + menu->mouseMoved(); + if (!menu->hasMouseMoved(e->globalPos())) { + return; + } + RippleButton::mouseMoveEvent(e); + updateCloseHovered(e->globalPos()); + return; + } + ItemBase::mouseMoveEvent(e); + updateCloseHovered(e->globalPos()); +} + +void WhoReactedEntryAction::mouseReleaseEvent(QMouseEvent *e) { + const auto menu = static_cast(parentWidget()); + if (!menu->hasMouseMoved(e->globalPos())) { + return; + } + if (!base::take(_closePressed)) { + ItemBase::mouseReleaseEvent(e); + updateCloseHovered(e->globalPos()); + return; + } + const auto overRow = rect().contains(e->pos()); + const auto overClose = closeAffordanceActive() + && overRow + && _closeRect.contains(e->pos()); + if (isOver()) { + setOver(false, StateChangeSource::ByPress); + } + if (isDown()) { + setDown( + false, + StateChangeSource::ByPress, + e->modifiers(), + e->button()); + } + if (overRow) { + setOver(true, StateChangeSource::ByHover); + } + updateCloseHovered(e->globalPos()); + if (overClose && _closeCallback) { + _closeCallback(); + } +} + +void WhoReactedEntryAction::resizeEvent(QResizeEvent *e) { + ItemBase::resizeEvent(e); + refreshCloseGeometry(); + invalidateCloseCache(); + updateCloseHovered(QCursor::pos()); +} + +QPoint WhoReactedEntryAction::prepareRippleStartPosition() const { + const auto result = mapFromGlobal(QCursor::pos()); + return (_closeRippleActive && !_closeRect.isEmpty()) + ? (result - _closeRect.topLeft()) + : result; +} + +QImage WhoReactedEntryAction::prepareRippleMask() const { + if (!_closeRippleActive || _closeRect.isEmpty()) { + return Ui::RippleAnimation::RectMask(size()); + } + if (_closeBadgeMask.isNull()) { + auto cache = GenerateWhoReactedCloseBadgeCache( + _closeRect.size(), + _st.itemBgOver, + _st.itemBgOver); + _closeBadgeMask = std::move(cache.mask); + } + return _closeBadgeMask; +} + +bool WhoReactedEntryAction::closeAffordanceActive() const { + return _closeCallback + && isSelected() + && (lastTriggeredSource() == Menu::TriggeredSource::Mouse); +} + +void WhoReactedEntryAction::refreshCloseMouseTracking() { + setMouseTracking(bool(_closeCallback) || !isSelected()); +} + +void WhoReactedEntryAction::refreshCloseGeometry() { + if (!_closeCallback) { + _closeRect = QRect(); + return; + } + _closeRect = QRect( + width() - st::whoReadClose.width, + 0, + st::whoReadClose.width, + st::whoReadClose.height); +} + +void WhoReactedEntryAction::updateCloseHovered(QPoint globalPosition) { + const auto hovered = closeAffordanceActive() + && _closeRect.contains(mapFromGlobal(globalPosition)); + if (_closeHovered == hovered) { + return; + } + _closeHovered = hovered; + finishAnimating(); + invalidateCloseCache(); + update(); +} + +void WhoReactedEntryAction::clearCloseState() { + _closeHovered = false; + _closePressed = false; + _closeRippleActive = false; + finishAnimating(); +} + +void WhoReactedEntryAction::invalidateCloseCache() { + _closeBadge = QImage(); + _closeBadgeMask = QImage(); +} + void WhoReactedEntryAction::setData(Data &&data) { setActionTriggered(std::move(data.callback)); + clearCloseState(); + _closeCallback = std::move(data.closeCallback); + if (!_closeCallback) { + _closeRect = QRect(); + invalidateCloseCache(); + } _userpic = std::move(data.userpic); _text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions); if (data.date.isEmpty()) { @@ -797,26 +1064,32 @@ void WhoReactedEntryAction::setData(Data &&data) { _text.maxWidth(), st::whoReadDateSkip + _date.maxWidth()); const auto &padding = _st.itemPadding; - const auto rightSkip = padding.right() - + (_custom ? (size + padding.right()) : 0); + const auto customRight = _custom ? (size + padding.right()) : 0; + const auto rightSkip = customRight; const auto goodWidth = st::defaultWhoRead.nameLeft + textWidth + rightSkip; const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax); _textWidth = w - (goodWidth - textWidth); setMinWidth(w); + refreshCloseGeometry(); + refreshCloseMouseTracking(); + invalidateCloseCache(); + updateCloseHovered(QCursor::pos()); update(); } void WhoReactedEntryAction::paint(Painter &&p) { const auto enabled = isEnabled(); - const auto selected = isSelected(); + const auto badgeShown = closeAffordanceActive(); + const auto closeHovered = badgeShown && _closeHovered; + const auto selected = isSelected() && !closeHovered; if (selected && _st.itemBgOver->c.alpha() < 255) { p.fillRect(0, 0, width(), _height, _st.itemBg); } const auto bg = selected ? _st.itemBgOver : _st.itemBg; p.fillRect(0, 0, width(), _height, bg); - if (enabled) { + if (enabled && (!_closeRippleActive || _closeRect.isEmpty())) { paintRipple(p, 0, 0); } const auto photoSize = st::defaultWhoRead.photoSize; @@ -938,12 +1211,37 @@ void WhoReactedEntryAction::paint(Painter &&p) { (height() - _customSize) / 2), }); } + if (badgeShown && !_closeRect.isEmpty()) { + if (_closeBadge.isNull()) { + auto cache = GenerateWhoReactedCloseBadgeCache( + _closeRect.size(), + _st.itemBgOver, + _st.itemBgOver); + _closeBadge = std::move(cache.badge); + _closeBadgeMask = std::move(cache.mask); + } + const auto blur = st::whoReadCloseBlurPadding; + p.drawImage( + _closeRect.topLeft() - QPoint(blur, blur), + _closeBadge); + if (enabled && _closeRippleActive) { + paintRipple(p, _closeRect.topLeft()); + } + const auto &icon = closeHovered + ? st::whoReadClose.iconOver + : st::whoReadClose.icon; + icon.paint( + p, + WhoReactedCloseIconPosition(_closeRect, icon), + width()); + } } bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) { return (a.id == b.id) && (a.name == b.name) && (a.date == b.date) + && (a.self == b.self) && (a.userpicKey == b.userpicKey); } @@ -956,13 +1254,15 @@ base::unique_qptr WhoReactedContextAction( rpl::producer content, CustomEmojiFactory factory, Fn participantChosen, - Fn showAllChosen) { + Fn showAllChosen, + Fn moderateReactionChosen) { return base::make_unique_q( menu, std::move(content), std::move(factory), std::move(participantChosen), - std::move(showAllChosen)); + std::move(showAllChosen), + std::move(moderateReactionChosen)); } base::unique_qptr WhenReadContextAction( @@ -978,10 +1278,12 @@ base::unique_qptr WhenReadContextAction( WhoReactedListMenu::WhoReactedListMenu( CustomEmojiFactory factory, Fn participantChosen, - Fn showAllChosen) + Fn showAllChosen, + Fn moderateReactionChosen) : _customEmojiFactory(std::move(factory)) , _participantChosen(std::move(participantChosen)) -, _showAllChosen(std::move(showAllChosen)) { +, _showAllChosen(std::move(showAllChosen)) +, _moderateReactionChosen(std::move(moderateReactionChosen)) { } void WhoReactedListMenu::clear() { @@ -1032,6 +1334,13 @@ void WhoReactedListMenu::populate( const auto chosen = [call = _participantChosen, participant] { call(participant); }; + const auto closeChosen = (!participant.customEntityData.isEmpty() + && _moderateReactionChosen + && !participant.self) + ? Fn([ + call = _moderateReactionChosen, + participant] { call(participant); }) + : Fn(); append({ .text = participant.name, .date = participant.date, @@ -1041,6 +1350,7 @@ void WhoReactedListMenu::populate( .customEntityData = participant.customEntityData, .userpic = participant.userpicLarge, .callback = chosen, + .closeCallback = std::move(closeChosen), }); } if (addShowAll) { diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index 2e0d1b14c1..baa55cb1b5 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -19,6 +19,7 @@ struct WhoReadParticipant { QString name; QString date; bool dateReacted = false; + bool self = false; QString customEntityData; QImage userpicSmall; QImage userpicLarge; @@ -62,7 +63,8 @@ struct WhoReadContent { rpl::producer content, Text::CustomEmojiFactory factory, Fn participantChosen, - Fn showAllChosen); + Fn showAllChosen, + Fn moderateReactionChosen = nullptr); [[nodiscard]] base::unique_qptr WhenReadContextAction( not_null menu, @@ -86,6 +88,7 @@ struct WhoReactedEntryData { QString customEntityData; QImage userpic; Fn callback; + Fn closeCallback; }; class WhoReactedEntryAction final : public Menu::ItemBase { @@ -105,8 +108,20 @@ public: private: int contentHeight() const override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; void paint(Painter &&p); + [[nodiscard]] bool closeAffordanceActive() const; + void refreshCloseMouseTracking(); + void refreshCloseGeometry(); + void updateCloseHovered(QPoint globalPosition); + void clearCloseState(); + void invalidateCloseCache(); const not_null _dummyAction; const Text::CustomEmojiFactory _customEmojiFactory; @@ -120,6 +135,13 @@ private: int _textWidth = 0; int _customSize = 0; WhoReactedType _type = WhoReactedType::Viewed; + Fn _closeCallback; + QRect _closeRect; + bool _closeHovered = false; + bool _closePressed = false; + bool _closeRippleActive = false; + mutable QImage _closeBadge; + mutable QImage _closeBadgeMask; }; @@ -128,7 +150,8 @@ public: WhoReactedListMenu( Text::CustomEmojiFactory factory, Fn participantChosen, - Fn showAllChosen); + Fn showAllChosen, + Fn moderateReactionChosen = nullptr); void clear(); void populate( @@ -142,6 +165,7 @@ private: const Text::CustomEmojiFactory _customEmojiFactory; const Fn _participantChosen; const Fn _showAllChosen; + const Fn _moderateReactionChosen; std::vector> _actions; From 49db3722960c682fc5aaae621fe11627f5096657 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 24 Apr 2026 16:29:38 +0700 Subject: [PATCH 104/154] Update cached reactions on remove. --- Telegram/SourceFiles/api/api_who_reacted.cpp | 64 ++++++++++++++++-- Telegram/SourceFiles/apiwrap.cpp | 20 +++++- Telegram/SourceFiles/apiwrap.h | 8 ++- .../boxes/moderate_messages_box.cpp | 11 +++- .../SourceFiles/boxes/moderate_messages_box.h | 3 + .../data/data_message_reaction_id.h | 12 ++++ .../data/data_message_reactions.cpp | 65 ++++++++++++++++++ .../SourceFiles/data/data_message_reactions.h | 3 + Telegram/SourceFiles/data/data_session.cpp | 36 ++++++++++ Telegram/SourceFiles/data/data_session.h | 16 +++++ Telegram/SourceFiles/history/history_item.cpp | 21 ++++++ Telegram/SourceFiles/history/history_item.h | 3 + .../view/history_view_context_menu.cpp | 3 +- .../reactions/history_view_reactions_list.cpp | 66 ++++++++++++++++++- .../reactions/history_view_reactions_list.h | 7 ++ .../controls/who_reacted_context_action.cpp | 6 +- .../ui/controls/who_reacted_context_action.h | 2 + 17 files changed, 329 insertions(+), 17 deletions(-) diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index ea4a3cd4f6..5cc833ec41 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -116,6 +116,7 @@ struct Userpic { TimeId date = 0; bool dateReacted = false; QString customEntityData; + ReactionId reaction; mutable Ui::PeerUserpicView view; mutable InMemoryKey uniqueKey; }; @@ -128,6 +129,26 @@ struct State { bool scheduled = false; }; +[[nodiscard]] bool ApplyReactionsRemovedToCachedData( + PeersWithReactions &data, + const Data::ReactionsRemoved &update) { + const auto was = data.list.size(); + data.list.erase( + ranges::remove_if(data.list, [&](const PeerWithReaction &entry) { + return !entry.reaction.empty() + && entry.peerWithDate.peer == update.participant->id; + }), + end(data.list)); + const auto removed = int(was - data.list.size()); + if (!removed) { + return false; + } + data.fullReactionsCount = (data.fullReactionsCount > removed) + ? (data.fullReactionsCount - removed) + : 0; + return true; +} + [[nodiscard]] auto Contexts() -> base::flat_map, std::unique_ptr> & { static auto result = base::flat_map< @@ -188,6 +209,22 @@ struct State { context->cachedReacted.erase(j); } }, context->subscriptions[session]); + session->data().reactionsRemoved( + ) | rpl::on_next([=](const Data::ReactionsRemoved &update) { + for (auto &[item, map] : context->cachedReacted) { + if (item->history()->peer->id != update.peer->id) { + continue; + } else if (update.msgId && item->id != update.msgId) { + continue; + } + for (auto &entry : map) { + auto data = entry.second.data.current(); + if (ApplyReactionsRemovedToCachedData(data, update)) { + entry.second.data = std::move(data); + } + } + } + }, context->subscriptions[session]); Data::AmPremiumValue( session ) | rpl::skip(1) | rpl::filter( @@ -443,12 +480,22 @@ bool UpdateUserpics( return resolved.peer != nullptr; }) | ranges::to_vector; - const auto same = ranges::equal( - state->userpics, - peers, - ranges::equal_to(), - [](const Userpic &u) { return std::pair(u.peer.get(), u.date); }, - [](const ResolvedPeer &r) { return std::pair(r.peer, r.date); }); + const auto same = [&] { + if (state->userpics.size() != peers.size()) { + return false; + } + const auto count = state->userpics.size(); + for (auto i = size_t(); i != count; ++i) { + const auto &userpic = state->userpics[i]; + const auto &resolved = peers[i]; + if ((userpic.peer.get() != resolved.peer) + || (userpic.date != resolved.date) + || (userpic.reaction != resolved.reaction)) { + return false; + } + } + return true; + }(); if (same) { return false; } @@ -461,6 +508,7 @@ bool UpdateUserpics( if (i != end(was) && i->view.cloud) { i->date = resolved.date; i->dateReacted = resolved.dateReacted; + i->reaction = resolved.reaction; now.push_back(std::move(*i)); now.back().customEntityData = data; continue; @@ -470,6 +518,7 @@ bool UpdateUserpics( .date = resolved.date, .dateReacted = resolved.dateReacted, .customEntityData = data, + .reaction = resolved.reaction, }); auto &userpic = now.back(); userpic.uniqueKey = peer->userpicUniqueKey(userpic.view); @@ -519,6 +568,8 @@ void RegenerateParticipants(not_null state, int small, int large) { was->date = FormatReadDate(date, currentDate); was->dateReacted = userpic.dateReacted; was->self = self; + was->customEntityData = userpic.customEntityData; + was->reaction = userpic.reaction; now.push_back(std::move(*was)); continue; } @@ -528,6 +579,7 @@ void RegenerateParticipants(not_null state, int small, int large) { .dateReacted = userpic.dateReacted, .self = self, .customEntityData = userpic.customEntityData, + .reaction = userpic.reaction, .userpicLarge = GenerateUserpic(userpic, large), .userpicKey = userpic.uniqueKey, .id = id, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index e3f4a13330..4638d7a828 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" +#include "data/data_message_reaction_id.h" #include "data/data_saved_messages.h" #include "data/data_saved_music.h" #include "data/data_saved_sublist.h" @@ -1424,7 +1425,15 @@ void ApiWrap::deleteAllFromParticipantSend( void ApiWrap::deleteAllReactionsFromParticipant( not_null peer, - not_null participant) { + not_null participant, + MsgId originMsgId, + const Data::ReactionId &originReaction) { + _session->data().removeReactionsFromParticipant( + peer, + 0, + participant, + originReaction, + originMsgId); request(MTPmessages_DeleteParticipantReactions( peer->input(), participant->input() @@ -1434,7 +1443,14 @@ void ApiWrap::deleteAllReactionsFromParticipant( void ApiWrap::deleteParticipantReaction( not_null peer, MsgId msgId, - not_null participant) { + not_null participant, + const Data::ReactionId &reaction) { + _session->data().removeReactionsFromParticipant( + peer, + msgId, + participant, + reaction, + 0); request(MTPmessages_DeleteParticipantReaction( peer->input(), MTP_int(msgId.bare), diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index dd98d792a7..4c577c37be 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -25,6 +25,7 @@ class Session; } // namespace Main namespace Data { +struct ReactionId; struct UpdatedFileReferences; class WallPaper; struct ResolvedForwardDraft; @@ -239,11 +240,14 @@ public: not_null from); void deleteAllReactionsFromParticipant( not_null peer, - not_null participant); + not_null participant, + MsgId originMsgId, + const Data::ReactionId &originReaction); void deleteParticipantReaction( not_null peer, MsgId msgId, - not_null participant); + not_null participant, + const Data::ReactionId &reaction); void deleteSublistHistory( not_null parentChat, not_null sublistPeer); diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 02aa6a8eee..8bd01b9df2 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -818,10 +818,16 @@ void CreateModerateMessagesBox( deleteReactionsController).empty()) { for (const auto &participant : deleteReactionsController->collectRequests()) { + const auto useOriginReaction = reaction + && (participant == reaction->participant); peer->session().api() .deleteAllReactionsFromParticipant( peer, - participant); + participant, + useOriginReaction ? reaction->msgId : MsgId(), + useOriginReaction + ? reaction->reaction + : Data::ReactionId()); } } }, deleteReactions->lifetime()); @@ -1265,7 +1271,8 @@ void CreateModerateMessagesBox( session->api().deleteParticipantReaction( reaction->peer, reaction->msgId, - reaction->participant); + reaction->participant, + reaction->reaction); } close(); }); diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.h b/Telegram/SourceFiles/boxes/moderate_messages_box.h index 1847d60ff1..bea05adcf8 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.h +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_message_reaction_id.h" + class PeerData; namespace Data { @@ -29,6 +31,7 @@ struct ModerateReactionEntry { not_null peer; MsgId msgId; not_null participant; + Data::ReactionId reaction; }; struct ModerateMessagesBoxEntry { diff --git a/Telegram/SourceFiles/data/data_message_reaction_id.h b/Telegram/SourceFiles/data/data_message_reaction_id.h index 5462ba65ec..5480179d48 100644 --- a/Telegram/SourceFiles/data/data_message_reaction_id.h +++ b/Telegram/SourceFiles/data/data_message_reaction_id.h @@ -7,8 +7,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/basic_types.h" #include "base/qt/qt_compare.h" +#include + +namespace tl { +template +class boxed; +} // namespace tl + +class MTPreaction; +using MTPReaction = tl::boxed; +using DocumentId = uint64; + namespace Data { struct ReactionId { diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 8c9536807d..117a5a3abc 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -2044,6 +2044,71 @@ void MessageReactions::remove(const ReactionId &id) { owner.notifyItemDataChange(_item); } +bool MessageReactions::removeFromParticipant( + not_null participant, + const ReactionId &knownReaction) { + auto changed = false; + auto participantFound = false; + const auto decrementReactionCount = [&](const ReactionId &id, int count) { + const auto i = ranges::find(_list, id, &MessageReaction::id); + if (i == end(_list)) { + return false; + } + if (i->count <= count) { + _list.erase(i); + } else { + i->count -= count; + } + return true; + }; + for (auto i = begin(_recent); i != end(_recent);) { + auto &list = i->second; + const auto was = int(list.size()); + list.erase( + ranges::remove(list, participant, &RecentReaction::peer), + end(list)); + if (const auto removed = was - int(list.size())) { + changed = true; + participantFound = true; + decrementReactionCount(i->first, removed); + } + if (list.empty()) { + i = _recent.erase(i); + } else { + ++i; + } + } + if (_paid) { + auto removedCount = 0; + auto removedEntries = 0; + _paid->top.erase( + ranges::remove_if(_paid->top, [&](const TopPaid &entry) { + if (entry.peer != participant.get()) { + return false; + } + removedCount += int(entry.count); + ++removedEntries; + return true; + }), + end(_paid->top)); + if (removedEntries) { + changed = true; + const auto paid = ReactionId::Paid(); + participantFound = true; + decrementReactionCount(paid, removedCount); + if (_paid->top.empty() && !localPaidData()) { + _paid = nullptr; + } + } + } + if (!knownReaction.empty() + && !participantFound + && decrementReactionCount(knownReaction, 1)) { + changed = true; + } + return changed; +} + bool MessageReactions::checkIfChanged( const QVector &list, const QVector &recent, diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 11da1fec9a..93cef12b08 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -405,6 +405,9 @@ public: void add(const ReactionId &id, bool addToRecent); void remove(const ReactionId &id); + bool removeFromParticipant( + not_null participant, + const ReactionId &knownReaction); bool change( const QVector &list, const QVector &recent, diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 74efd0bd79..4d30915673 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -2092,6 +2092,14 @@ rpl::producer> Session::itemDataChanges() const { return _itemDataChanges.events(); } +void Session::notifyReactionsRemoved(ReactionsRemoved update) { + _reactionsRemoved.fire(std::move(update)); +} + +rpl::producer Session::reactionsRemoved() const { + return _reactionsRemoved.events(); +} + void Session::requestItemTextRefresh(not_null item) { const auto call = [&](not_null item) { enumerateItemViews(item, [&](not_null view) { @@ -3033,6 +3041,34 @@ HistoryItem *Session::message(FullMsgId itemId) const { return message(itemId.peer, itemId.msg); } +void Session::removeReactionsFromParticipant( + not_null peer, + MsgId msgId, + not_null participant, + const ReactionId &reaction, + MsgId originMsgId) { + if (msgId) { + if (const auto item = message(peer, msgId)) { + item->removeReactionsFromParticipant(participant, reaction); + } + } else if (const auto list = messagesList(peer->id)) { + for (const auto &entry : *list) { + const auto knownReaction = (originMsgId + && (entry.second->id == originMsgId)) + ? reaction + : ReactionId(); + entry.second->removeReactionsFromParticipant( + participant, + knownReaction); + } + } + notifyReactionsRemoved({ + .peer = peer, + .msgId = msgId, + .participant = participant, + }); +} + HistoryItem *Session::nonChannelMessage(MsgId itemId) const { if (!IsServerMsgId(itemId)) { return nullptr; diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index bcff8e4982..eee6d4ae20 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -148,6 +148,12 @@ struct DrawToReplyRequest { uint64 documentId = 0; }; +struct ReactionsRemoved { + not_null peer; + MsgId msgId = 0; + not_null participant; +}; + struct RequestViewRepaint { not_null view; QRect rect; @@ -415,6 +421,8 @@ public: [[nodiscard]] rpl::producer> historyUnloaded() const; void notifyItemDataChange(not_null item); [[nodiscard]] rpl::producer> itemDataChanges() const; + void notifyReactionsRemoved(ReactionsRemoved update); + [[nodiscard]] rpl::producer reactionsRemoved() const; [[nodiscard]] rpl::producer> itemRemoved() const; [[nodiscard]] rpl::producer> itemRemoved( @@ -572,6 +580,13 @@ public: PeerId peerId, const QVector &data); + void removeReactionsFromParticipant( + not_null peer, + MsgId msgId, + not_null participant, + const ReactionId &reaction, + MsgId originMsgId); + [[nodiscard]] MsgId nextLocalMessageId(); [[nodiscard]] HistoryItem *message( PeerId peerId, @@ -1154,6 +1169,7 @@ private: rpl::event_stream> _itemTextRefreshRequest; rpl::event_stream _drawToReplyRequests; rpl::event_stream> _itemDataChanges; + rpl::event_stream _reactionsRemoved; rpl::event_stream> _itemRemoved; rpl::event_stream> _viewRemoved; rpl::event_stream> _viewPaidReactionSent; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index e37fa97a8c..abebb21f25 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3203,6 +3203,27 @@ void HistoryItem::toggleReaction( _history->owner().notifyItemDataChange(this); } +bool HistoryItem::removeReactionsFromParticipant( + not_null participant, + const Data::ReactionId &reaction) { + if (!_reactions) { + return false; + } + const auto hadUnread = hasUnreadReaction(); + if (!_reactions->removeFromParticipant(participant, reaction)) { + return false; + } + if (_reactions->empty() && !_reactions->localPaidData()) { + _reactions = nullptr; + _flags &= ~MessageFlag::CanViewReactions; + } + if (hadUnread && (!_reactions || !_reactions->hasUnread())) { + markReactionsRead(); + } + _history->owner().notifyItemDataChange(this); + return true; +} + void HistoryItem::updateReactionsUnknown() { _reactionsLastRefreshed = 1; } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 1da3599ef8..771605f715 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -485,6 +485,9 @@ public: void toggleReaction( const Data::ReactionId &reaction, HistoryReactionSource source); + bool removeReactionsFromParticipant( + not_null participant, + const Data::ReactionId &reaction); void addPaidReaction(int count, std::optional shownPeer = {}); void cancelScheduledPaidReaction(); [[nodiscard]] Data::PaidReactionSend startPaidReactionSending(); diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index df17b091ad..dbaed5f6bc 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1199,7 +1199,8 @@ void EditTagBox( controller, item->history()->peer, itemId.msg, - participant); + participant, + who.reaction); }; } diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp index 5605a11f74..73c16d55cd 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp @@ -119,6 +119,7 @@ private: not_null peer, ReactionId reaction) const; void showReaction(const ReactionId &reaction); + void applyReactionsRemoved(const Data::ReactionsRemoved &update); [[nodiscard]] uint64 id( not_null peer, @@ -249,6 +250,13 @@ Controller::Controller( }) | rpl::on_next([=](const ReactionId &reaction) { showReaction(reaction); }, lifetime()); + session().data().reactionsRemoved( + ) | rpl::filter([=](const Data::ReactionsRemoved &update) { + return ((update.peer == _peer) || (update.peer->id == _peer->id)) + && (!update.msgId || update.msgId == _itemId.msg); + }) | rpl::on_next([=](const Data::ReactionsRemoved &update) { + applyReactionsRemoved(update); + }, lifetime()); } Main::Session &Controller::session() const { @@ -302,6 +310,40 @@ void Controller::showReaction(const ReactionId &reaction) { delegate()->peerListRefreshRows(); } +void Controller::applyReactionsRemoved( + const Data::ReactionsRemoved &update) { + const auto participantId = update.participant->id; + + const auto allWas = _all.size(); + _all.erase( + ranges::remove_if(_all, [&](const AllEntry &entry) { + return entry.first->id == participantId; + }), + end(_all)); + + const auto filteredWas = _filtered.size(); + _filtered.erase( + ranges::remove_if(_filtered, [&](not_null peer) { + return peer->id == participantId; + }), + end(_filtered)); + + auto removed = (allWas != _all.size()) + || (filteredWas != _filtered.size()); + for (auto i = delegate()->peerListFullRowsCount(); i != 0;) { + const auto row = delegate()->peerListRowAt(--i); + if (row->peer()->id == participantId + && static_cast(row.get())->isReactionRow()) { + delegate()->peerListRemoveRow(row); + removed = true; + } + } + if (removed) { + setDescriptionText(QString()); + delegate()->peerListRefreshRows(); + } +} + uint64 Controller::id( not_null peer, const ReactionId &reaction) const { @@ -420,9 +462,12 @@ void Controller::loadMore(const ReactionId &reaction) { reaction.match([&](const MTPDmessagePeerReaction &data) { const auto peer = sessionData->peerLoaded( peerFromMTP(data.vpeer_id())); + if (!peer) { + return; + } const auto reaction = Data::ReactionFromMTP( data.vreaction()); - if (peer && (!shown || appendRow(peer, reaction))) { + if (!shown || appendRow(peer, reaction)) { if (filtered) { _filtered.emplace_back(peer); } else { @@ -465,6 +510,7 @@ base::unique_qptr Controller::rowContextMenu( || !CanModerateReactionByDeleteMessages(_peer)) { return nullptr; } + const auto reaction = reactionRow->reaction(); auto result = base::make_unique_q( parent, @@ -476,7 +522,8 @@ base::unique_qptr Controller::rowContextMenu( _window->parentController(), _peer, _itemId.msg, - participant); + participant, + reaction); }, .icon = &st::menuIconDeleteAttention, .isAttention = true, @@ -522,6 +569,20 @@ void ShowModerateReactionBox( not_null originPeer, MsgId originMsgId, not_null participant) { + ShowModerateReactionBox( + controller, + originPeer, + originMsgId, + participant, + Data::ReactionId()); +} + +void ShowModerateReactionBox( + not_null controller, + not_null originPeer, + MsgId originMsgId, + not_null participant, + Data::ReactionId reaction) { controller->show(Box( CreateModerateMessagesBox, ModerateMessagesBoxEntry{ @@ -529,6 +590,7 @@ void ShowModerateReactionBox( .peer = originPeer, .msgId = originMsgId, .participant = participant, + .reaction = std::move(reaction), }, }, nullptr, diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h index 745915bc32..b1cbd347d3 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h @@ -63,6 +63,13 @@ void ShowModerateReactionBox( MsgId originMsgId, not_null participant); +void ShowModerateReactionBox( + not_null controller, + not_null originPeer, + MsgId originMsgId, + not_null participant, + Data::ReactionId reaction); + void ShowReactionParticipantInfo( not_null window, not_null participant, diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index 08062aa21a..b657d323b9 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -1064,8 +1064,8 @@ void WhoReactedEntryAction::setData(Data &&data) { _text.maxWidth(), st::whoReadDateSkip + _date.maxWidth()); const auto &padding = _st.itemPadding; - const auto customRight = _custom ? (size + padding.right()) : 0; - const auto rightSkip = customRight; + const auto rightSkip = padding.right() + + (_custom ? (size + padding.right()) : 0); const auto goodWidth = st::defaultWhoRead.nameLeft + textWidth + rightSkip; @@ -1242,6 +1242,8 @@ bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) { && (a.name == b.name) && (a.date == b.date) && (a.self == b.self) + && (a.customEntityData == b.customEntityData) + && (a.reaction == b.reaction) && (a.userpicKey == b.userpicKey); } diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index baa55cb1b5..cd3cbf7877 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" #include "ui/widgets/menu/menu_item_base.h" #include "ui/text/text_custom_emoji.h" +#include "data/data_message_reaction_id.h" namespace Ui { @@ -21,6 +22,7 @@ struct WhoReadParticipant { bool dateReacted = false; bool self = false; QString customEntityData; + Data::ReactionId reaction; QImage userpicSmall; QImage userpicLarge; std::pair userpicKey = {}; From ef9a976923292d82fba74bf642b86fc4a8373d0b Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 24 Apr 2026 22:38:05 +0700 Subject: [PATCH 105/154] Allow to remove installed styles. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/boxes/compose_ai_box.cpp | 35 +++--- .../SourceFiles/boxes/create_ai_tone_box.cpp | 23 ++-- .../data/data_ai_compose_tones.cpp | 104 +++++++++++++++--- .../SourceFiles/data/data_ai_compose_tones.h | 15 +++ 5 files changed, 142 insertions(+), 37 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c691e21866..d750bd8bf3 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7946,7 +7946,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_prompt_placeholder" = "Instructions (for example \"write in bold, nautical tone, light slang (aye, matey), vivid sea imagery, playful swagger, rhythmic phrasing, and adventurous mood\")"; "lng_ai_compose_tone_edit" = "Edit Style"; "lng_ai_compose_tone_share" = "Share Style"; +"lng_ai_compose_tone_remove" = "Remove Style"; "lng_ai_compose_tone_delete" = "Delete Style"; +"lng_ai_compose_tone_remove_sure" = "Are you sure you want to remove this style?"; "lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style? It will be removed for everyone who installed it."; "lng_ai_compose_tone_link_copied" = "Style link copied."; "lng_ai_compose_author" = "Style by {user}"; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 88e378de60..b99581da09 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -1099,6 +1099,9 @@ void ComposeAiContent::refreshTones() { } } _styleIndex = remapped; + if (_mode == ComposeAiMode::Style && hadSelection && _styleIndex < 0) { + request(); + } } void ComposeAiContent::selectToneById(uint64 id) { @@ -1649,25 +1652,27 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { return; } const auto &tone = tones[index]; - if (!tone.creator) { + if (tone.isDefault) { return; } *contextMenu = base::make_unique_q( ptr->entity(), st::popupMenuWithIcons); const auto toneCopy = tone; - (*contextMenu)->addAction( - tr::lng_ai_compose_tone_edit(tr::now), - [=] { - box->uiShow()->show(Box( - EditAiToneBox, - session, - toneCopy, - crl::guard(content, [=](Data::AiComposeTone tone) { - content->selectToneById(tone.id); - }))); - }, - &st::menuIconEdit); + if (toneCopy.creator) { + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_edit(tr::now), + [=] { + box->uiShow()->show(Box( + EditAiToneBox, + session, + toneCopy, + crl::guard(content, [=](Data::AiComposeTone tone) { + content->selectToneById(tone.id); + }))); + }, + &st::menuIconEdit); + } (*contextMenu)->addAction( tr::lng_ai_compose_tone_share(tr::now), [=] { @@ -1683,7 +1688,9 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { st::menuWithIconsAttention, Ui::Menu::CreateAction( (*contextMenu)->menu().get(), - tr::lng_ai_compose_tone_delete(tr::now), + toneCopy.creator + ? tr::lng_ai_compose_tone_delete(tr::now) + : tr::lng_ai_compose_tone_remove(tr::now), [=] { ConfirmDeleteAiTone( box->uiShow(), diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index b65b9e591f..f1ba4c7682 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -614,8 +614,7 @@ void EditAiToneBox( not_null session, const Data::AiComposeTone &tone, Fn saved) { - const auto toneId = tone.id; - const auto toneAccessHash = tone.accessHash; + const auto toneCopy = tone; SetupToneBox( box, session, @@ -629,9 +628,6 @@ void EditAiToneBox( const QString &name, const QString &prompt, bool displayAuthor) { - auto toneCopy = Data::AiComposeTone(); - toneCopy.id = toneId; - toneCopy.accessHash = toneAccessHash; session->data().aiComposeTones().update( toneCopy, name, @@ -648,9 +644,6 @@ void EditAiToneBox( })); }, [=] { - auto toneCopy = Data::AiComposeTone(); - toneCopy.id = toneId; - toneCopy.accessHash = toneAccessHash; ConfirmDeleteAiTone( box->uiShow(), session, @@ -664,6 +657,20 @@ void ConfirmDeleteAiTone( not_null session, const Data::AiComposeTone &tone, Fn done) { + if (!tone.creator) { + show->show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_remove_sure(), + .confirmed = [=](Fn &&close) { + close(); + session->data().aiComposeTones().save( + tone, + true, + done); + }, + .confirmText = tr::lng_box_remove(), + })); + return; + } show->show(Ui::MakeConfirmBox({ .text = tr::lng_ai_compose_tone_delete_sure(), .confirmed = [=](Fn &&close) { diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp index 11fe92a065..678672e7a2 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -27,11 +27,20 @@ AiComposeTones::AiComposeTones(not_null session) } void AiComposeTones::refresh() { + refreshWithHash(_hash); +} + +void AiComposeTones::refreshWithHash(uint64 hash) { if (_refreshRequestId) { + if (hash == 0) { + _pendingRefresh = PendingRefresh::Full; + } else if (_pendingRefresh == PendingRefresh::None) { + _pendingRefresh = PendingRefresh::Incremental; + } return; } _refreshRequestId = _session->api().request(MTPaicompose_GetTones( - MTP_long(_hash) + MTP_long(hash) )).done([=](const MTPaicompose_Tones &result) { _refreshRequestId = 0; result.match([&](const MTPDaicompose_tones &data) { @@ -41,8 +50,10 @@ void AiComposeTones::refresh() { _updates.fire({}); }, [](const MTPDaicompose_tonesNotModified &) { }); + finishRefresh(); }).fail([=] { _refreshRequestId = 0; + finishRefresh(); }).send(); } @@ -52,6 +63,7 @@ void AiComposeTones::parseTones(const QVector &list) { for (const auto &tone : list) { _list.push_back(parseTone(tone)); } + reapplyRecentCustomToneOrder(); } AiComposeTone AiComposeTones::parseTone( @@ -108,7 +120,7 @@ void AiComposeTones::create( MTP_string(prompt) )).done([=](const MTPAiComposeTone &result) { auto parsed = parseTone(result); - _list.push_back(parsed); + promoteCustomTone(parsed); _hash = 0; _updates.fire({}); if (done) { @@ -155,12 +167,7 @@ void AiComposeTones::update( MTP_string(prompt.value_or(QString())) )).done([=](const MTPAiComposeTone &result) { auto parsed = parseTone(result); - const auto i = ranges::find(_list, parsed.id, &AiComposeTone::id); - if (i != end(_list)) { - *i = parsed; - } else { - _list.push_back(parsed); - } + promoteCustomTone(parsed); _hash = 0; _updates.fire({}); if (done) { @@ -183,6 +190,14 @@ void AiComposeTones::save( toneToMTP(tone), unsave ? MTP_boolTrue() : MTP_boolFalse() )).done([=] { + if (unsave) { + removeCustomTone(tone.id); + forgetRecentCustomTone(tone.id); + } else { + promoteCustomTone(tone); + } + _hash = 0; + _updates.fire({}); if (done) { done(); } @@ -202,13 +217,8 @@ void AiComposeTones::remove( toneToMTP(tone) )).done([=] { if (!toneCopy.isDefault) { - const auto i = ranges::find( - _list, - toneCopy.id, - &AiComposeTone::id); - if (i != end(_list)) { - _list.erase(i); - } + removeCustomTone(toneCopy.id); + forgetRecentCustomTone(toneCopy.id); } _hash = 0; _updates.fire({}); @@ -281,6 +291,70 @@ void AiComposeTones::applyUpdate() { refresh(); } +void AiComposeTones::finishRefresh() { + const auto pending = _pendingRefresh; + _pendingRefresh = PendingRefresh::None; + if (pending == PendingRefresh::None) { + return; + } + refreshWithHash((pending == PendingRefresh::Full) ? 0 : _hash); +} + +void AiComposeTones::promoteCustomTone(AiComposeTone tone) { + if (tone.isDefault) { + return; + } + removeCustomTone(tone.id); + rememberRecentCustomTone(tone.id); + _list.insert(begin(_list), std::move(tone)); +} + +void AiComposeTones::removeCustomTone(uint64 id) { + const auto i = ranges::find(_list, id, &AiComposeTone::id); + if (i != end(_list)) { + _list.erase(i); + } +} + +void AiComposeTones::rememberRecentCustomTone(uint64 id) { + const auto i = ranges::find(_recentCustomToneIds, id); + if (i != end(_recentCustomToneIds)) { + _recentCustomToneIds.erase(i); + } + _recentCustomToneIds.insert(begin(_recentCustomToneIds), id); +} + +void AiComposeTones::forgetRecentCustomTone(uint64 id) { + const auto i = ranges::find(_recentCustomToneIds, id); + if (i != end(_recentCustomToneIds)) { + _recentCustomToneIds.erase(i); + } +} + +void AiComposeTones::reapplyRecentCustomToneOrder() { + auto reordered = std::vector(); + reordered.reserve(_list.size()); + + auto recent = std::vector(); + recent.reserve(_recentCustomToneIds.size()); + for (const auto id : _recentCustomToneIds) { + const auto i = ranges::find(_list, id, &AiComposeTone::id); + if (i != end(_list) && !i->isDefault) { + reordered.push_back(*i); + recent.push_back(id); + } + } + for (const auto &tone : _list) { + const auto promoted = !tone.isDefault + && (ranges::find(recent, tone.id) != end(recent)); + if (!promoted) { + reordered.push_back(tone); + } + } + _recentCustomToneIds = std::move(recent); + _list = std::move(reordered); +} + MTPInputAiComposeTone AiComposeTones::toneToMTP( const AiComposeTone &tone) const { return tone.isDefault diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.h b/Telegram/SourceFiles/data/data_ai_compose_tones.h index 141597760d..fe4a050f3e 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.h +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.h @@ -90,13 +90,28 @@ private: void parseTones(const QVector &list); [[nodiscard]] AiComposeTone parseTone( const MTPAiComposeTone &tone) const; + void finishRefresh(); + void promoteCustomTone(AiComposeTone tone); + void removeCustomTone(uint64 id); + void rememberRecentCustomTone(uint64 id); + void forgetRecentCustomTone(uint64 id); + void reapplyRecentCustomToneOrder(); + void refreshWithHash(uint64 hash); + + enum class PendingRefresh { + None, + Incremental, + Full, + }; const not_null _session; uint64 _hash = 0; mtpRequestId _refreshRequestId = 0; std::vector _list; + std::vector _recentCustomToneIds; rpl::event_stream<> _updates; base::Timer _refreshTimer; + PendingRefresh _pendingRefresh = PendingRefresh::None; }; From 04dbe641cf27457670e1c88af3a9a9621256c48d Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 25 Apr 2026 00:03:26 +0700 Subject: [PATCH 106/154] Remove caption position control for files. --- Telegram/SourceFiles/boxes/send_files_box.cpp | 46 ++++++++++++++++--- Telegram/SourceFiles/boxes/send_files_box.h | 8 +++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 78b30651fa..b40b9fde92 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -750,10 +750,8 @@ Fn SendFilesBox::prepareSendMenuDetails( ? SendMenu::SpoilerState::Enabled : SendMenu::SpoilerState::Possible; const auto way = _sendWay.current(); - const auto canMoveCaption = _list.canMoveCaption( - way.groupFiles() && way.sendImagesAsPhotos(), - way.sendImagesAsPhotos() - ) && HasSendText(_caption); + const auto canMoveCaption = canMoveCaptionInCurrentSendWay() + && HasSendText(_caption); result.caption = !canMoveCaption ? SendMenu::CaptionState::None : _invertCaption @@ -885,7 +883,7 @@ void SendFilesBox::setupDragArea() { const auto droppedCallback = [=](bool compress) { return [=](const QMimeData *data) { - addFiles(data); + addFiles(data, compress); _show->activate(); }; }; @@ -1033,6 +1031,23 @@ bool SendFilesBox::hasSendLargePhotosOption() const { _sendWay.current().sendImagesAsPhotos()); } +bool SendFilesBox::canMoveCaptionInCurrentSendWay() const { + const auto way = _sendWay.current(); + if (!way.sendImagesAsPhotos() || !_list.canAddCaption(true)) { + return false; + } + const auto count = int(_list.files.size()); + if (count < 1 || count > Ui::MaxAlbumItems()) { + return false; + } + const auto isPhotoOrVideo = [](const Ui::PreparedFile &file) { + return file.type == Ui::PreparedFile::Type::Photo + || file.type == Ui::PreparedFile::Type::Video; + }; + return (count == 1 || way.groupFiles()) + && ranges::all_of(_list.files, isPhotoOrVideo); +} + bool SendFilesBox::canChangePrice() const { const auto way = _sendWay.current(); const auto broadcast = _toPeer->asBroadcast(); @@ -2086,7 +2101,9 @@ void SendFilesBox::captionResized() { update(); } -bool SendFilesBox::addFiles(not_null data) { +bool SendFilesBox::addFiles( + not_null data, + std::optional overrideSendImagesAsPhotos) { const auto premium = _show->session().premium(); auto list = [&] { const auto urls = Core::ReadMimeUrls(data); @@ -2108,13 +2125,30 @@ bool SendFilesBox::addFiles(not_null data) { } return result; }(); + if (overrideSendImagesAsPhotos.has_value()) { + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + } return addFiles(std::move(list)); } +void SendFilesBox::applySendImagesAsPhotosOverride( + const Ui::PreparedList &list) { + if (!list.overrideSendImagesAsPhotos.has_value()) { + return; + } + _list.overrideSendImagesAsPhotos = list.overrideSendImagesAsPhotos; + auto candidate = _sendWay.current(); + candidate.setSendImagesAsPhotos(*list.overrideSendImagesAsPhotos); + if (checkWith(list, candidate, true)) { + _sendWay = candidate; + } +} + bool SendFilesBox::addFiles(Ui::PreparedList list) { if (list.error != Ui::PreparedList::Error::None) { return false; } + applySendImagesAsPhotosOverride(list); const auto count = int(_list.files.size()); _list.filesToProcess.insert( _list.filesToProcess.end(), diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index f752bb1c7b..4aad174c08 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -225,9 +225,10 @@ private: void setSendLargePhotos(bool enabled); void changePrice(); - [[nodiscard]] bool canChangePrice() const; [[nodiscard]] bool hasPrice() const; [[nodiscard]] bool hasSendLargePhotosOption() const; + [[nodiscard]] bool canMoveCaptionInCurrentSendWay() const; + [[nodiscard]] bool canChangePrice() const; void refreshPriceTag(); [[nodiscard]] QImage preparePriceTagBg(QSize size) const; @@ -256,7 +257,10 @@ private: void updateControlsGeometry(); void updateCaptionVisibility(); - bool addFiles(not_null data); + bool addFiles( + not_null data, + std::optional overrideSendImagesAsPhotos = std::nullopt); + void applySendImagesAsPhotosOverride(const Ui::PreparedList &list); bool addFiles(Ui::PreparedList list); void addFile(Ui::PreparedFile &&file); void pushBlock(int from, int till); From c5b63c9339da30d04190f32f6b19e0c5dc067c38 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Apr 2026 19:15:38 +0700 Subject: [PATCH 107/154] Improve single author case in moderation. --- Telegram/Resources/langs/lang.strings | 5 +- .../boxes/moderate_messages_box.cpp | 437 ++++++++++++++++-- .../boxes/peers/edit_peer_permissions_box.cpp | 2 +- 3 files changed, 390 insertions(+), 54 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d750bd8bf3..57cd018995 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4575,6 +4575,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_audio_count#other" = "{count} audio"; "lng_ban_user" = "Ban User"; +"lng_ban_specific_user" = "Ban {user}"; "lng_ban_users" = "Ban users"; "lng_restrict_users" = "Restrict users"; "lng_delete_all_from_user" = "Delete all from {user}"; @@ -6330,8 +6331,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_chat_send_media" = "Send media"; "lng_rights_chat_send_stickers" = "Send stickers & GIFs"; "lng_rights_chat_send_links" = "Embed links"; -"lng_rights_chat_send_reactions" = "Send reactions"; -"lng_rights_chat_send_polls" = "Send polls"; +"lng_rights_chat_send_reactions" = "Reactions"; +"lng_rights_chat_send_polls" = "Polls"; "lng_rights_chat_add_members" = "Add members"; "lng_rights_chat_photos" = "Photos"; "lng_rights_chat_videos" = "Video files"; diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 8bd01b9df2..5716522117 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/toggle_arrow.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rect.h" @@ -44,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_lottie_custom_emoji.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/expandable_peer_list.h" #include "ui/widgets/participants_check_view.h" @@ -178,6 +180,166 @@ ModerateOptions CalculateModerateOptions(const ModerateReactionEntry &reaction) || options.banOrRestrict; } +class DeleteOptionsCheckView final : public Ui::AbstractCheckView { +public: + DeleteOptionsCheckView( + int checkedCount, + int totalCount, + int duration, + bool expanded, + Fn updateCallback); + + [[nodiscard]] static QString Text(int checkedCount, int totalCount); + [[nodiscard]] static QSize ComputeSize(int totalCount); + + void setCheckedCount(int count); + + QSize getSize() const override; + QImage prepareRippleMask() const override; + bool checkRippleStartPosition(QPoint position) const override; + void paint(QPainter &p, int left, int top, int outerWidth) override; + +private: + void checkedChangedHook(anim::type animated) override; + + int _checkedCount = 0; + int _totalCount = 0; + QString _text; + +}; + +DeleteOptionsCheckView::DeleteOptionsCheckView( + int checkedCount, + int totalCount, + int duration, + bool expanded, + Fn updateCallback) +: Ui::AbstractCheckView(duration, expanded, std::move(updateCallback)) +, _checkedCount(checkedCount) +, _totalCount(totalCount) +, _text(Text(checkedCount, totalCount)) { +} + +QString DeleteOptionsCheckView::Text(int checkedCount, int totalCount) { + return u"%1 / %2"_q.arg(checkedCount).arg(totalCount); +} + +QSize DeleteOptionsCheckView::ComputeSize(int totalCount) { + return QSize( + st::moderateBoxExpandHeight + + st::moderateBoxExpandInnerSkip * 4 + + st::moderateBoxExpandFont->width(Text(totalCount, totalCount)) + + st::moderateBoxExpandToggleSize, + st::moderateBoxExpandHeight); +} + +void DeleteOptionsCheckView::setCheckedCount(int count) { + _checkedCount = count; + _text = Text(_checkedCount, _totalCount); + update(); +} + +QSize DeleteOptionsCheckView::getSize() const { + return ComputeSize(_totalCount); +} + +QImage DeleteOptionsCheckView::prepareRippleMask() const { + const auto size = getSize(); + return Ui::RippleAnimation::RoundRectMask(size, size.height() / 2); +} + +bool DeleteOptionsCheckView::checkRippleStartPosition(QPoint position) const { + return Rect(getSize()).contains(position); +} + +void DeleteOptionsCheckView::paint( + QPainter &p, + int left, + int top, + int outerWidth) { + auto hq = PainterHighQualityEnabler(p); + const auto size = getSize(); + const auto radius = size.height() / 2; + const auto innerSkip = st::moderateBoxExpandInnerSkip; + + p.setBrush(Qt::NoBrush); + p.setPen(st::boxTextFg); + p.setFont(st::moderateBoxExpandFont); + p.drawText( + QRect( + left + innerSkip + radius, + top, + size.width(), + size.height()), + _text, + style::al_left); + + const auto path = Ui::ToggleUpDownArrowPath( + left + size.width() - st::moderateBoxExpandToggleSize - radius, + top + size.height() / 2, + st::moderateBoxExpandToggleSize, + st::moderateBoxExpandToggleFourStrokes, + currentAnimationValue()); + p.fillPath(path, st::boxTextFg); +} + +void DeleteOptionsCheckView::checkedChangedHook(anim::type animated) { +} + +class DeleteOptionsButton final : public Ui::RippleButton { +public: + DeleteOptionsButton( + not_null parent, + int checkedCount, + int totalCount); + + void setCheckedCount(int count); + [[nodiscard]] not_null checkView() const; + +private: + void paintEvent(QPaintEvent *event) override; + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + + std::unique_ptr _view; + +}; + +DeleteOptionsButton::DeleteOptionsButton( + not_null parent, + int checkedCount, + int totalCount) +: Ui::RippleButton(parent, st::defaultRippleAnimation) +, _view(std::make_unique( + checkedCount, + totalCount, + st::slideWrapDuration, + false, + [=] { update(); })) { +} + +void DeleteOptionsButton::setCheckedCount(int count) { + _view->setCheckedCount(count); +} + +not_null DeleteOptionsButton::checkView() const { + return _view.get(); +} + +QImage DeleteOptionsButton::prepareRippleMask() const { + return _view->prepareRippleMask(); +} + +QPoint DeleteOptionsButton::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()); +} + +void DeleteOptionsButton::paintEvent(QPaintEvent *event) { + auto p = QPainter(this); + Ui::RippleButton::paintRipple(p, QPoint()); + _view->paint(p, 0, 0, width()); +} + [[nodiscard]] rpl::producer> MessagesCountValue( not_null history, std::vector> from) { @@ -646,9 +808,10 @@ void CreateModerateMessagesBox( return base::EventFilterResult::Continue; }); - const auto handleSubmition = [=](not_null checkbox) { - base::install_event_filter(box, [=](not_null event) { - if (!isEnter(event) || !checkbox->checked()) { + const auto handleSubmitionIf = [=](Fn enabled) { + base::install_event_filter(box, [=, enabled = std::move(enabled)]( + not_null event) { + if (!isEnter(event) || !enabled()) { return base::EventFilterResult::Continue; } box->uiShow()->show(Ui::MakeConfirmBox({ @@ -663,6 +826,12 @@ void CreateModerateMessagesBox( return base::EventFilterResult::Cancel; }); }; + const auto handleSubmition = [=](not_null checkbox) { + handleSubmitionIf([=] { + return checkbox->checked(); + }); + }; + Ui::Checkbox *deleteOptions = nullptr; Ui::Checkbox *deleteMessages = nullptr; Controller *deleteMessagesController = nullptr; rpl::variable> *deleteMessagesCounts = nullptr; @@ -741,6 +910,9 @@ void CreateModerateMessagesBox( const auto showMessagesCheckbox = deleteAllMessages; const auto showReactionsCheckbox = deleteAllReactions; + const auto useSingleDeleteOptions = isSingle + && showMessagesCheckbox + && showReactionsCheckbox; if (showMessagesCheckbox || showReactionsCheckbox) { Ui::AddSkip(inner); Ui::AddSkip(inner); @@ -748,7 +920,8 @@ void CreateModerateMessagesBox( ? participantIds : std::vector(); - if (showMessagesCheckbox) { + if (useSingleDeleteOptions) { + const auto participant = participants.front(); Assert(history != nullptr); deleteMessagesCounts = box->lifetime().make_state< rpl::variable>>( @@ -762,21 +935,118 @@ void CreateModerateMessagesBox( deleteMessagesController = box->lifetime().make_state( Controller::Data{ .messagesCounts = deleteMessagesCounts->value(), - .participants = participants, + .participants = Participants{ participant }, .checked = checkedParticipants, }); - deleteMessages = inner->add( + deleteReactionsController = box->lifetime().make_state( + Controller::Data{ + .participants = Participants{ participant }, + .checked = checkedParticipants, + }); + + const auto deleteOptionsSize = DeleteOptionsCheckView::ComputeSize(2); + const auto deleteOptionsPadding = QMargins( + 0, + 0, + deleteOptionsSize.width(), + 0); + deleteOptions = inner->add( object_ptr( inner, + tr::lng_delete_all_from_user( + lt_user, + rpl::single(participant->shortName())), + options.deleteAll, + st::defaultBoxCheckbox), + st::boxRowPadding + deleteOptionsPadding); + const auto button = Ui::CreateChild( + inner, + options.deleteAll ? 2 : 0, + 2); + button->resize(deleteOptionsSize); + deleteOptions->geometryValue( + ) | rpl::on_next([=](const QRect &rect) { + button->moveToRight( + st::moderateBoxExpandRight, + rect.top() + (rect.height() - button->height()) / 2, + inner->width()); + button->raise(); + }, button->lifetime()); + + const auto wrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + wrap->toggle(false, anim::type::instant); + button->setClickedCallback([=] { + button->checkView()->setChecked( + !button->checkView()->checked(), + anim::type::normal); + wrap->toggle( + button->checkView()->checked(), + anim::type::normal); + }); + + const auto container = wrap->entity(); + const auto optionCheckRect = deleteOptions->checkRect(); + const auto childOptionPadding = st::boxRowPadding + + QMargins( + optionCheckRect.width() + + st::defaultBoxCheckbox.textPosition.x() + - optionCheckRect.x(), + 0, + 0, + 0); + Ui::AddSkip(container); + Ui::AddSkip(container); + deleteMessages = container->add( + object_ptr( + container, tr::lng_delete_sub_messages(tr::now), options.deleteAll, st::defaultBoxCheckbox), - st::boxRowPadding + buttonPadding); - Ui::AddExpandablePeerList( - not_null{ deleteMessages }, - not_null{ deleteMessagesController }, - inner); - handleSubmition(not_null{ deleteMessages }); + childOptionPadding); + Ui::AddSkip(container); + Ui::AddSkip(container); + deleteReactions = container->add( + object_ptr( + container, + tr::lng_delete_sub_reactions(tr::now), + options.deleteAll, + st::defaultBoxCheckbox), + childOptionPadding); + deleteMessagesController->collectRequests = [=] { + return deleteMessages->checked() + ? Participants{ participant } + : Participants(); + }; + deleteReactionsController->collectRequests = [=] { + return deleteReactions->checked() + ? Participants{ participant } + : Participants(); + }; + const auto updateDeleteOptions = [=] { + const auto count = (deleteMessages->checked() ? 1 : 0) + + (deleteReactions->checked() ? 1 : 0); + deleteOptions->setChecked( + count == 2, + Ui::Checkbox::NotifyAboutChange::DontNotify); + button->setCheckedCount(count); + }; + deleteOptions->checkedChanges( + ) | rpl::on_next([=](bool checked) { + deleteMessages->setChecked(checked); + deleteReactions->setChecked(checked); + updateDeleteOptions(); + }, deleteOptions->lifetime()); + deleteMessages->checkedChanges( + ) | rpl::on_next(updateDeleteOptions, deleteMessages->lifetime()); + deleteReactions->checkedChanges( + ) | rpl::on_next(updateDeleteOptions, deleteReactions->lifetime()); + handleSubmitionIf([=] { + return deleteMessages->checked() + || deleteReactions->checked(); + }); handleConfirmation( not_null{ deleteMessages }, not_null{ deleteMessagesController }, @@ -785,31 +1055,6 @@ void CreateModerateMessagesBox( not_null c) { p->session().api().deleteAllFromParticipant(c, p); }); - } - - if (deleteMessages && showReactionsCheckbox) { - Ui::AddSkip(inner); - Ui::AddSkip(inner); - } - - if (showReactionsCheckbox) { - deleteReactionsController = box->lifetime().make_state( - Controller::Data{ - .participants = participants, - .checked = checkedParticipants, - }); - deleteReactions = inner->add( - object_ptr( - inner, - tr::lng_delete_sub_reactions(tr::now), - options.deleteAll, - st::defaultBoxCheckbox), - st::boxRowPadding + buttonPadding); - Ui::AddExpandablePeerList( - not_null{ deleteReactions }, - not_null{ deleteReactionsController }, - inner); - handleSubmition(not_null{ deleteReactions }); confirms->events() | rpl::on_next([=] { if (deleteReactions->checked() && deleteReactionsController->collectRequests @@ -820,17 +1065,105 @@ void CreateModerateMessagesBox( : deleteReactionsController->collectRequests()) { const auto useOriginReaction = reaction && (participant == reaction->participant); - peer->session().api() - .deleteAllReactionsFromParticipant( - peer, - participant, - useOriginReaction ? reaction->msgId : MsgId(), - useOriginReaction - ? reaction->reaction - : Data::ReactionId()); + const auto originMsgId = useOriginReaction + ? reaction->msgId + : MsgId(); + const auto originReaction = useOriginReaction + ? reaction->reaction + : Data::ReactionId(); + peer->session().api().deleteAllReactionsFromParticipant( + peer, + participant, + originMsgId, + originReaction); } } }, deleteReactions->lifetime()); + } else { + if (showMessagesCheckbox) { + Assert(history != nullptr); + deleteMessagesCounts = box->lifetime().make_state< + rpl::variable>>( + base::flat_map()); + MessagesCountValue( + history, + participants + ) | rpl::on_next([=](base::flat_map counts) { + deleteMessagesCounts->force_assign(std::move(counts)); + }, box->lifetime()); + deleteMessagesController = box->lifetime().make_state( + Controller::Data{ + .messagesCounts = deleteMessagesCounts->value(), + .participants = participants, + .checked = checkedParticipants, + }); + deleteMessages = inner->add( + object_ptr( + inner, + tr::lng_delete_sub_messages(tr::now), + options.deleteAll, + st::defaultBoxCheckbox), + st::boxRowPadding + buttonPadding); + Ui::AddExpandablePeerList( + not_null{ deleteMessages }, + not_null{ deleteMessagesController }, + inner); + handleSubmition(not_null{ deleteMessages }); + handleConfirmation( + not_null{ deleteMessages }, + not_null{ deleteMessagesController }, + [=]( + not_null p, + not_null c) { + p->session().api().deleteAllFromParticipant(c, p); + }); + } + + if (deleteMessages && showReactionsCheckbox) { + Ui::AddSkip(inner); + Ui::AddSkip(inner); + } + + if (showReactionsCheckbox) { + deleteReactionsController = box->lifetime().make_state( + Controller::Data{ + .participants = participants, + .checked = checkedParticipants, + }); + deleteReactions = inner->add( + object_ptr( + inner, + tr::lng_delete_sub_reactions(tr::now), + options.deleteAll, + st::defaultBoxCheckbox), + st::boxRowPadding + buttonPadding); + Ui::AddExpandablePeerList( + not_null{ deleteReactions }, + not_null{ deleteReactionsController }, + inner); + handleSubmition(not_null{ deleteReactions }); + confirms->events() | rpl::on_next([=] { + if (deleteReactions->checked() + && deleteReactionsController->collectRequests + && !effectiveCheckedParticipants( + deleteReactions, + deleteReactionsController).empty()) { + for (const auto &participant + : deleteReactionsController->collectRequests()) { + const auto useOriginReaction = reaction + && (participant == reaction->participant); + peer->session().api() + .deleteAllReactionsFromParticipant( + peer, + participant, + useOriginReaction ? reaction->msgId : MsgId(), + useOriginReaction + ? reaction->reaction + : Data::ReactionId()); + } + } + }, deleteReactions->lifetime()); + } } } const auto makeTitleLoadingDescriptor = [] { @@ -1078,7 +1411,9 @@ void CreateModerateMessagesBox( rpl::single(participants.size()) | tr::to_count()), rpl::conditional( rpl::single(isSingle), - tr::lng_ban_user(), + tr::lng_ban_specific_user( + lt_user, + rpl::single(participants.front()->shortName())), tr::lng_ban_users())), options.banUser, st::defaultBoxCheckbox), @@ -1134,12 +1469,12 @@ void CreateModerateMessagesBox( wrap->toggledValue( ) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) { return ((toggled && isSingle) - ? tr::lng_restrict_user_part - : (toggled && !isSingle) - ? tr::lng_restrict_users_part - : isSingle ? tr::lng_restrict_user_full - : tr::lng_restrict_users_full)( + : (toggled && !isSingle) + ? tr::lng_restrict_users_full + : isSingle + ? tr::lng_restrict_user_part + : tr::lng_restrict_users_part)( lt_emoji, rpl::single(toggled ? emojiUp : emojiDown), tr::marked); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 78eb5c2435..e9c0d0aa86 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -91,6 +91,7 @@ constexpr auto kDefaultChargeStars = 10; | Flag::SendInline, tr::lng_rights_chat_stickers(tr::now) }, { Flag::EmbedLinks, tr::lng_rights_chat_send_links(tr::now) }, { Flag::SendPolls, tr::lng_rights_chat_send_polls(tr::now) }, + { Flag::SendReactions, tr::lng_rights_chat_send_reactions(tr::now) }, }; auto second = std::vector{ { Flag::AddParticipants, tr::lng_rights_chat_add_members(tr::now) }, @@ -100,7 +101,6 @@ constexpr auto kDefaultChargeStars = 10; ? tr::lng_rights_group_edit_rank_single : tr::lng_rights_group_edit_rank)(tr::now) }, { Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) }, - { Flag::SendReactions, tr::lng_rights_chat_send_reactions(tr::now) }, }; if (!options.isForum) { second.erase( From 2ddc6ed1be7986740ee11bf05d1fc3120886ae80 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Apr 2026 21:09:45 +0700 Subject: [PATCH 108/154] Deduplicated small right toggle button. --- Telegram/SourceFiles/boxes/boxes.style | 9 +- .../boxes/moderate_messages_box.cpp | 181 ++---------------- .../ui/widgets/expandable_peer_list.cpp | 53 ++--- .../ui/widgets/participants_check_view.cpp | 135 ++++++++----- .../ui/widgets/participants_check_view.h | 39 +++- 5 files changed, 158 insertions(+), 259 deletions(-) diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 7b2fdad992..958a993858 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1088,11 +1088,18 @@ moderateBoxUserpic: UserpicButton(defaultUserpicButton) { photoSize: 34px; photoPosition: point(0px, 4px); } -moderateBoxExpand: icon {{ "chat/reply_type_group", boxTextFg }}; +moderateBoxExpand: IconEmoji { + icon: icon {{ "chat/reply_type_group", boxTextFg }}; + padding: margins(1px, 3px, 1px, 0px); + useIconColor: true; +} moderateBoxExpandHeight: 20px; moderateBoxExpandRight: 10px; moderateBoxExpandInnerSkip: 2px; moderateBoxExpandFont: font(11px); +moderateBoxExpandTextStyle: TextStyle(boxTextStyle) { + font: moderateBoxExpandFont; +} moderateBoxExpandToggleSize: 4px; moderateBoxExpandToggleFourStrokes: 3px; moderateBoxExpandIcon: IconEmoji{ diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 5716522117..eafbfc6db9 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -180,164 +180,16 @@ ModerateOptions CalculateModerateOptions(const ModerateReactionEntry &reaction) || options.banOrRestrict; } -class DeleteOptionsCheckView final : public Ui::AbstractCheckView { -public: - DeleteOptionsCheckView( +[[nodiscard]] TextWithEntities ParticipantsExpanderText(int count) { + return tr::marked() + .append(st::moderateBoxExpand) + .append(QString::number(count)); +} + +[[nodiscard]] TextWithEntities DeleteOptionsExpanderText( int checkedCount, - int totalCount, - int duration, - bool expanded, - Fn updateCallback); - - [[nodiscard]] static QString Text(int checkedCount, int totalCount); - [[nodiscard]] static QSize ComputeSize(int totalCount); - - void setCheckedCount(int count); - - QSize getSize() const override; - QImage prepareRippleMask() const override; - bool checkRippleStartPosition(QPoint position) const override; - void paint(QPainter &p, int left, int top, int outerWidth) override; - -private: - void checkedChangedHook(anim::type animated) override; - - int _checkedCount = 0; - int _totalCount = 0; - QString _text; - -}; - -DeleteOptionsCheckView::DeleteOptionsCheckView( - int checkedCount, - int totalCount, - int duration, - bool expanded, - Fn updateCallback) -: Ui::AbstractCheckView(duration, expanded, std::move(updateCallback)) -, _checkedCount(checkedCount) -, _totalCount(totalCount) -, _text(Text(checkedCount, totalCount)) { -} - -QString DeleteOptionsCheckView::Text(int checkedCount, int totalCount) { - return u"%1 / %2"_q.arg(checkedCount).arg(totalCount); -} - -QSize DeleteOptionsCheckView::ComputeSize(int totalCount) { - return QSize( - st::moderateBoxExpandHeight - + st::moderateBoxExpandInnerSkip * 4 - + st::moderateBoxExpandFont->width(Text(totalCount, totalCount)) - + st::moderateBoxExpandToggleSize, - st::moderateBoxExpandHeight); -} - -void DeleteOptionsCheckView::setCheckedCount(int count) { - _checkedCount = count; - _text = Text(_checkedCount, _totalCount); - update(); -} - -QSize DeleteOptionsCheckView::getSize() const { - return ComputeSize(_totalCount); -} - -QImage DeleteOptionsCheckView::prepareRippleMask() const { - const auto size = getSize(); - return Ui::RippleAnimation::RoundRectMask(size, size.height() / 2); -} - -bool DeleteOptionsCheckView::checkRippleStartPosition(QPoint position) const { - return Rect(getSize()).contains(position); -} - -void DeleteOptionsCheckView::paint( - QPainter &p, - int left, - int top, - int outerWidth) { - auto hq = PainterHighQualityEnabler(p); - const auto size = getSize(); - const auto radius = size.height() / 2; - const auto innerSkip = st::moderateBoxExpandInnerSkip; - - p.setBrush(Qt::NoBrush); - p.setPen(st::boxTextFg); - p.setFont(st::moderateBoxExpandFont); - p.drawText( - QRect( - left + innerSkip + radius, - top, - size.width(), - size.height()), - _text, - style::al_left); - - const auto path = Ui::ToggleUpDownArrowPath( - left + size.width() - st::moderateBoxExpandToggleSize - radius, - top + size.height() / 2, - st::moderateBoxExpandToggleSize, - st::moderateBoxExpandToggleFourStrokes, - currentAnimationValue()); - p.fillPath(path, st::boxTextFg); -} - -void DeleteOptionsCheckView::checkedChangedHook(anim::type animated) { -} - -class DeleteOptionsButton final : public Ui::RippleButton { -public: - DeleteOptionsButton( - not_null parent, - int checkedCount, - int totalCount); - - void setCheckedCount(int count); - [[nodiscard]] not_null checkView() const; - -private: - void paintEvent(QPaintEvent *event) override; - QImage prepareRippleMask() const override; - QPoint prepareRippleStartPosition() const override; - - std::unique_ptr _view; - -}; - -DeleteOptionsButton::DeleteOptionsButton( - not_null parent, - int checkedCount, - int totalCount) -: Ui::RippleButton(parent, st::defaultRippleAnimation) -, _view(std::make_unique( - checkedCount, - totalCount, - st::slideWrapDuration, - false, - [=] { update(); })) { -} - -void DeleteOptionsButton::setCheckedCount(int count) { - _view->setCheckedCount(count); -} - -not_null DeleteOptionsButton::checkView() const { - return _view.get(); -} - -QImage DeleteOptionsButton::prepareRippleMask() const { - return _view->prepareRippleMask(); -} - -QPoint DeleteOptionsButton::prepareRippleStartPosition() const { - return mapFromGlobal(QCursor::pos()); -} - -void DeleteOptionsButton::paintEvent(QPaintEvent *event) { - auto p = QPainter(this); - Ui::RippleButton::paintRipple(p, QPoint()); - _view->paint(p, 0, 0, width()); + int totalCount) { + return tr::marked(u"%1 / %2"_q.arg(checkedCount).arg(totalCount)); } [[nodiscard]] rpl::producer> MessagesCountValue( @@ -595,8 +447,8 @@ void CreateModerateMessagesBox( : QMargins( 0, 0, - Ui::ParticipantsCheckView::ComputeSize( - participants.size()).width(), + Ui::ExpanderButton::ComputeSize( + ParticipantsExpanderText(int(participants.size()))).width(), 0); const auto firstItem = hasItems ? items.front().get() : nullptr; @@ -944,7 +796,8 @@ void CreateModerateMessagesBox( .checked = checkedParticipants, }); - const auto deleteOptionsSize = DeleteOptionsCheckView::ComputeSize(2); + const auto deleteOptionsSize = Ui::ExpanderButton::ComputeSize( + DeleteOptionsExpanderText(2, 2)); const auto deleteOptionsPadding = QMargins( 0, 0, @@ -959,10 +812,9 @@ void CreateModerateMessagesBox( options.deleteAll, st::defaultBoxCheckbox), st::boxRowPadding + deleteOptionsPadding); - const auto button = Ui::CreateChild( + const auto button = Ui::CreateChild( inner, - options.deleteAll ? 2 : 0, - 2); + DeleteOptionsExpanderText(2, 2)); button->resize(deleteOptionsSize); deleteOptions->geometryValue( ) | rpl::on_next([=](const QRect &rect) { @@ -1031,7 +883,7 @@ void CreateModerateMessagesBox( deleteOptions->setChecked( count == 2, Ui::Checkbox::NotifyAboutChange::DontNotify); - button->setCheckedCount(count); + button->setText(DeleteOptionsExpanderText(count, 2)); }; deleteOptions->checkedChanges( ) | rpl::on_next([=](bool checked) { @@ -1043,6 +895,7 @@ void CreateModerateMessagesBox( ) | rpl::on_next(updateDeleteOptions, deleteMessages->lifetime()); deleteReactions->checkedChanges( ) | rpl::on_next(updateDeleteOptions, deleteReactions->lifetime()); + updateDeleteOptions(); handleSubmitionIf([=] { return deleteMessages->checked() || deleteReactions->checked(); diff --git a/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp b/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp index 1093a97b39..f879f9a757 100644 --- a/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp +++ b/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp @@ -22,49 +22,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_widgets.h" +#include + namespace Ui { namespace { -class Button final : public Ui::RippleButton { -public: - Button(not_null parent, int count); - - [[nodiscard]] not_null checkView() const; - -private: - void paintEvent(QPaintEvent *event) override; - QImage prepareRippleMask() const override; - QPoint prepareRippleStartPosition() const override; - - std::unique_ptr _view; - -}; - -Button::Button(not_null parent, int count) -: Ui::RippleButton(parent, st::defaultRippleAnimation) -, _view(std::make_unique( - count, - st::slideWrapDuration, - false, - [=] { update(); })) { -} - -not_null Button::checkView() const { - return _view.get(); -} - -QImage Button::prepareRippleMask() const { - return _view->prepareRippleMask(); -} - -QPoint Button::prepareRippleStartPosition() const { - return mapFromGlobal(QCursor::pos()); -} - -void Button::paintEvent(QPaintEvent *event) { - auto p = QPainter(this); - Ui::RippleButton::paintRipple(p, QPoint()); - _view->paint(p, 0, 0, width()); +[[nodiscard]] TextWithEntities ParticipantsExpanderText(int count) { + return tr::marked() + .append(st::moderateBoxExpand) + .append(QString::number(std::abs(count))); } } // namespace @@ -86,10 +52,13 @@ void AddExpandablePeerList( } const auto count = int(participants.size()); const auto button = !hideRightButton - ? Ui::CreateChild