From 443053b927444a50c41a51718717b413be6f4878 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 5 Oct 2025 15:51:55 +0300 Subject: [PATCH] Added simple sending animation for text messages to history widget. --- .../chat_helpers/message_field.cpp | 1 + .../SourceFiles/history/history_widget.cpp | 13 +++- Telegram/SourceFiles/ui/chat/chat.style | 2 + Telegram/SourceFiles/ui/chat/chat_style.h | 2 +- .../message_sending_animation_common.h | 1 + .../message_sending_animation_controller.cpp | 75 ++++++++++++++----- 6 files changed, 72 insertions(+), 22 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 6fdf722bbf..7422b4e35e 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -576,6 +576,7 @@ void InitMessageFieldGeometry(not_null field) { st::historySendSize.height() - 2 * st::historySendPadding); field->setMaxHeight(st::historyComposeFieldMaxHeight); + // st::messageSendingAnimationTextFromOffset. field->setDocumentMargin(4.); field->setAdditionalMargin(style::ConvertScale(4) - 4); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 04f55a977c..ebad00626f 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4752,10 +4752,20 @@ void HistoryWidget::send(Api::SendOptions options) { return; } + const auto nextLocalMessageId = session().data().nextLocalMessageId(); + + if (HasSendText(_field) && message.webPage.url.isEmpty()) { + controller()->sendingAnimation().appendSending({ + .type = Ui::MessageSendingAnimationFrom::Type::Text, + .localId = nextLocalMessageId, + .globalStartGeometry = _field->mapToGlobal(Rect(_field->size())), + }); + } + // Just a flag not to drop reply info if we're not sending anything. _justMarkingAsRead = !HasSendText(_field) && message.webPage.url.isEmpty(); - session().api().sendMessage(std::move(message)); + session().api().sendMessage(std::move(message), nextLocalMessageId); _justMarkingAsRead = false; clearFieldText(); @@ -7141,7 +7151,6 @@ void HistoryWidget::startMessageSendingAnimation( return; } Assert(item->mainView() != nullptr); - Assert(item->mainView()->media() != nullptr); auto globalEndTopLeft = rpl::merge( _scroll->innerResizes() | rpl::to_empty, diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 43da6f922c..1a59ef2088 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1396,6 +1396,8 @@ chatTabsOutlineHorizontal: ChatTabsOutline { chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) { } +messageSendingAnimationTextFromOffset: point(-6px, 9px); + suggestPriceBox: Box(defaultBox) { buttonPadding: margins(22px, 22px, 22px, 22px); buttonHeight: 42px; diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h index f8718f7484..5499fd97eb 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.h +++ b/Telegram/SourceFiles/ui/chat/chat_style.h @@ -243,7 +243,7 @@ struct ChatPaintContext { }; - // This is supported only in unwrapped media for now. + // This is supported only in unwrapped media and text messages for now. enum class SkipDrawingParts { None, Content, diff --git a/Telegram/SourceFiles/ui/effects/message_sending_animation_common.h b/Telegram/SourceFiles/ui/effects/message_sending_animation_common.h index a3877b615b..a64b9fcbcf 100644 --- a/Telegram/SourceFiles/ui/effects/message_sending_animation_common.h +++ b/Telegram/SourceFiles/ui/effects/message_sending_animation_common.h @@ -15,6 +15,7 @@ struct MessageSendingAnimationFrom { Sticker, Gif, Emoji, + Text, }; Type type = Type::None; std::optional localId; diff --git a/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp b/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp index 552f464122..0b11e3ef70 100644 --- a/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp +++ b/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animation_value_f.h" #include "ui/effects/animations.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/rp_widget.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" @@ -58,6 +59,7 @@ private: const not_null _controller; const bool _crop; + const bool _isText; MessageSendingAnimationController::SendingInfoTo _toInfo; QRect _from; QPoint _to; @@ -84,10 +86,18 @@ Content::Content( : RpWidget(parent) , _controller(controller) , _crop(fromInfo.crop) +, _isText(fromInfo.type == MessageSendingAnimationFrom::Type::Text) , _toInfo(std::move(to)) , _from(parent->mapFromGlobal(fromInfo.globalStartGeometry)) -, _innerContentRect(maybeView()->media()->contentRectForReactions()) -, _minScale(float64(_from.height()) / _innerContentRect.height()) { +, _innerContentRect(_isText + ? [&] { + const auto g = maybeView()->innerGeometry(); + return Rect(g.size()) + QMargins(0, g.top(), 0, 0); + }() + : maybeView()->media()->contentRectForReactions()) +, _minScale(!_isText + ? float64(_from.height()) / _innerContentRect.height() + : 1.) { Expects(_toInfo.view != nullptr); Expects(_toInfo.paintContext != nullptr); @@ -117,24 +127,41 @@ Content::Content( auto animationCallback = [=](float64 value) { auto resultFrom = rect(); - resultFrom.moveCenter(_from.center()); + if (_isText) { + resultFrom.moveTo( + style::RightToLeft() + ? (rect::right(_from) - resultFrom.width()) + : _from.left(), + _from.y()); + // Calculating relative values for all possible cases + // is too complex, so we manually set the position + // for the simplest case. + resultFrom.translate(st::messageSendingAnimationTextFromOffset); + } else { + resultFrom.moveCenter(_from.center()); + } - const auto resultTo = _to - + innerGeometry.topLeft() - + _innerContentRect.topLeft(); + const auto resultTo = _isText + ? _to + innerGeometry.topLeft() + : _to + innerGeometry.topLeft() + _innerContentRect.topLeft(); const auto x = anim::interpolate(resultFrom.x(), resultTo.x(), value); const auto y = anim::interpolate(resultFrom.y(), resultTo.y(), value); - moveToLeft(x, y); - update(); + if (!_isText) { + // Text-only messages are drawing only in _bubble.widget. + moveToLeft(x, y); + update(); + } else { + setUpdatesEnabled(false); + } - if ((value > kSurroundingProgress) + if ((value > kSurroundingProgress || _isText) && !_surrounding && !_bubble.widget) { const auto currentView = maybeView(); if (!checkView(currentView)) { return; } - if (currentView->hasBubble()) { + if (_isText || currentView->hasBubble()) { createBubble(); } else { createSurrounding(); @@ -280,12 +307,12 @@ void Content::createSurrounding() { const auto alpha = (divider - revProgress) / divider; p.setOpacity(alpha); - const auto scale = anim::interpolateF(_minScale, 1., progress); + const auto scale = anim::interpolateF(_minScale, 1., progress); - p.translate( - revProgress * OffsetMid(size.width() + offset.x(), _minScale), - revProgress * OffsetMid(size.height() + offset.y(), _minScale)); - p.scale(scale, scale); + p.translate( + revProgress * OffsetMid(size.width() + offset.x(), _minScale), + revProgress * OffsetMid(size.height() + offset.y(), _minScale)); + p.scale(scale, scale); const auto currentView = maybeView(); if (!checkView(currentView)) { @@ -312,7 +339,11 @@ void Content::createBubble() { const auto tailWidth = st::historyBubbleTailOutLeft.width(); _bubble.offsetFromContent = QPoint( - currentView->hasOutLayout() ? 0 : tailWidth, + (currentView->hasOutLayout() + && (currentView->delegate()->elementChatMode() + != HistoryView::ElementChatMode::Wide)) + ? 0 + : tailWidth, innerGeometry.y()); const auto scaleOffset = QPoint(0, innerGeometry.y()); @@ -323,8 +354,11 @@ void Content::createBubble() { || currentView->data()->externalReply(); _bubble.widget->resize(innerGeometry.size() + QSize( - currentView->hasOutLayout() ? tailWidth : 0, - hasCommentsButton ? innerGeometry.y() : 0)); + (currentView->hasOutLayout() ? tailWidth : 0) + + (_isText && currentView->data()->isPost() + ? rect::m::sum::h(st::msgPadding) + : 0), + (hasCommentsButton || _isText) ? innerGeometry.y() : 0)); _bubble.widget->show(); _bubble.widget->stackUnder(this); @@ -338,7 +372,7 @@ void Content::createBubble() { const auto progress = _animation.value(0.); const auto revProgress = 1. - progress; - const auto divider = 1. - kSurroundingProgress; + const auto divider = 1. - (_isText ? 0. : kSurroundingProgress); const auto alpha = (divider - revProgress) / divider; p.setOpacity(alpha); @@ -443,6 +477,7 @@ bool MessageSendingAnimationController::checkExpectedType( const auto type = it->second.type; const auto isSticker = type == MessageSendingAnimationFrom::Type::Sticker; const auto isGif = type == MessageSendingAnimationFrom::Type::Gif; + const auto isText = type == MessageSendingAnimationFrom::Type::Text; if (isSticker || isGif) { if (item->emptyText()) { if (const auto media = item->media()) { @@ -454,6 +489,8 @@ bool MessageSendingAnimationController::checkExpectedType( } } } + } else if (isText && !item->media()) { + return true; } _itemSendPending.erase(it); return false;