diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index a380b6d5f..d0d59bb5a 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -149,6 +149,7 @@ EmojiButton { SendButton { inner: IconButton; + stars: RoundButton; record: icon; recordOver: icon; round: icon; @@ -1293,6 +1294,12 @@ historySend: SendButton { icon: historySendIcon; iconOver: historySendIconOver; } + stars: RoundButton(defaultActiveButton) { + height: 28px; + padding: margins(0px, 0px, 6px, 0px); + textTop: 5px; + width: -8px; + } record: historyRecordVoice; recordOver: historyRecordVoiceOver; round: historyRecordRound; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 964c08888..c862ccc5e 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -491,6 +491,11 @@ HistoryWidget::HistoryWidget( moveFieldControls(); }, lifetime()); + _send->widthValue() | rpl::skip(1) | rpl::start_with_next([=] { + updateFieldSize(); + moveFieldControls(); + }, _send->lifetime()); + _keyboard->sendCommandRequests( ) | rpl::start_with_next([=](Bot::SendCommandRequest r) { sendBotCommand(r); @@ -810,6 +815,7 @@ HistoryWidget::HistoryWidget( }) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) { if (flags & PeerUpdateFlag::Rights) { updateFieldPlaceholder(); + updateSendButtonType(); _preview->checkNow(false); const auto was = (_sendAs != nullptr); @@ -839,6 +845,7 @@ HistoryWidget::HistoryWidget( } if (flags & PeerUpdateFlag::StarsPerMessage) { updateFieldPlaceholder(); + updateSendButtonType(); } if (flags & PeerUpdateFlag::BotStartToken) { updateControlsVisibility(); @@ -4138,7 +4145,7 @@ void HistoryWidget::checkReplyReturns() { } void HistoryWidget::cancelInlineBot() { - auto &textWithTags = _field->getTextWithTags(); + const auto &textWithTags = _field->getTextWithTags(); if (textWithTags.text.size() > _inlineBotUsername.size() + 2) { setFieldText( { '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, @@ -5134,19 +5141,27 @@ void HistoryWidget::updateSendButtonType() { using Type = Ui::SendButton::Type; const auto type = computeSendButtonType(); - _send->setType(type); - // This logic is duplicated in RepliesWidget. const auto disabledBySlowmode = _peer && _peer->slowmodeApplied() && (_history->latestSendingMessage() != nullptr); - const auto delay = [&] { return (type != Type::Cancel && type != Type::Save && _peer) ? _peer->slowmodeSecondsLeft() : 0; }(); - _send->setSlowmodeDelay(delay); + const auto perMessage = _peer ? _peer->starsPerMessageChecked() : 0; + const auto stars = perMessage + ? perMessage * ComputeSendingMessagesCount(_history, { + .forward = &_forwardPanel->items(), + .text = &_field->getTextWithTags(), + }) + : 0; + _send->setState({ + .type = (delay > 0) ? Type::Slowmode : type, + .slowmodeDelay = delay, + .starsToSend = stars, + }); _send->setDisabled(disabledBySlowmode && (type == Type::Send || type == Type::Record 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 dc323ba7d..1fee75916 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -2164,6 +2164,10 @@ void ComposeControls::initSendButton() { _recordAvailability = value; updateSendButtonType(); }, _send->lifetime()); + + _send->widthValue() | rpl::skip(1) | rpl::start_with_next([=] { + updateControlsGeometry(_wrap->size()); + }, _send->lifetime()); } void ComposeControls::initSendAsButton(not_null peer) { @@ -2550,14 +2554,18 @@ SendMenu::Details ComposeControls::sendButtonMenuDetails() const { void ComposeControls::updateSendButtonType() { using Type = Ui::SendButton::Type; const auto type = computeSendButtonType(); - _send->setType(type); - const auto delay = [&] { return (type != Type::Cancel && type != Type::Save) ? _slowmodeSecondsLeft.current() : 0; }(); - _send->setSlowmodeDelay(delay); + const auto peer = _history ? _history->peer.get() : nullptr; + const auto stars = peer ? peer->starsPerMessageChecked() : 0; + _send->setState({ + .type = type, + .slowmodeDelay = delay, + .starsToSend = stars, + }); _send->setDisabled(_sendDisabledBySlowmode.current() && (type == Type::Send || type == Type::Record diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 1f80fb15c..696a480b1 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -524,6 +524,7 @@ public: ListenWrap( not_null parent, const style::RecordBar &st, + std::shared_ptr send, not_null session, not_null data, const style::font &font); @@ -551,6 +552,7 @@ private: const not_null _parent; const style::RecordBar &_st; + const std::shared_ptr _send; const not_null _session; const not_null _document; const std::unique_ptr _voiceData; @@ -585,11 +587,13 @@ private: ListenWrap::ListenWrap( not_null parent, const style::RecordBar &st, + std::shared_ptr send, not_null session, not_null data, const style::font &font) : _parent(parent) , _st(st) +, _send(send) , _session(session) , _document(DummyDocument(&session->data())) , _voiceData(ProcessCaptureResult(data->waveform)) @@ -614,14 +618,18 @@ void ListenWrap::init() { }) | rpl::distinct_until_changed(); _delete->showOn(std::move(deleteShow)); - _parent->sizeValue( - ) | rpl::start_with_next([=](QSize size) { + rpl::combine( + _parent->sizeValue(), + _send->widthValue() + ) | rpl::start_with_next([=](QSize size, int send) { _waveformBgRect = QRect({ 0, 0 }, size) .marginsRemoved(st::historyRecordWaveformBgMargins); { - const auto m = _st.remove.width + _waveformBgRect.height() / 2; + const auto skip = _waveformBgRect.height() / 2; + const auto left = _st.remove.width + skip; + const auto right = send + skip; _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved( - style::margins(m, 0, m, 0)); + style::margins(left, 0, right, 0)); } { const auto &play = _playPauseSt.playOuter; @@ -651,7 +659,7 @@ void ListenWrap::init() { const auto deleteIconLeft = remove.iconPosition.x(); const auto bgRectRight = anim::interpolate( deleteIconLeft, - remove.width, + _send->width(), _isShowAnimation ? progress : 1.); const auto bgRectLeft = anim::interpolate( _parent->width() - deleteIconLeft - _waveformBgRect.height(), @@ -1973,6 +1981,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { _listen = std::make_unique( this, _st, + _send, &_show->session(), &_data, _cancelFont); @@ -2006,6 +2015,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { _listen = std::make_unique( this, _st, + _send, &_show->session(), &_data, _cancelFont); diff --git a/Telegram/SourceFiles/ui/controls/send_button.cpp b/Telegram/SourceFiles/ui/controls/send_button.cpp index e0388c032..5aaa78303 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.cpp +++ b/Telegram/SourceFiles/ui/controls/send_button.cpp @@ -7,10 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/controls/send_button.h" +#include "lang/lang_tag.h" #include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" // starIconEmoji namespace Ui { namespace { @@ -22,47 +25,58 @@ constexpr int kWideScale = 5; SendButton::SendButton(QWidget *parent, const style::SendButton &st) : RippleButton(parent, st.inner.ripple) , _st(st) { - resize(_st.inner.width, _st.inner.height); + updateSize(); } -void SendButton::setType(Type type) { - Expects(isSlowmode() || type != Type::Slowmode); - - if (isSlowmode() && type != Type::Slowmode) { - _afterSlowmodeType = type; +void SendButton::setState(State state) { + if (_state == state) { return; } - if (_type != type) { + const auto hasSlowmode = (_state.slowmodeDelay > 0); + const auto hasSlowmodeChanged = hasSlowmode != (state.slowmodeDelay > 0); + auto withSameSlowmode = state; + withSameSlowmode.slowmodeDelay = _state.slowmodeDelay; + const auto animate = hasSlowmodeChanged + || (!hasSlowmode && withSameSlowmode != _state); + if (animate) { _contentFrom = grabContent(); - _type = type; - _a_typeChanged.stop(); + } + if (_state.slowmodeDelay != state.slowmodeDelay) { + const auto seconds = state.slowmodeDelay; + const auto minutes = seconds / 60; + _slowmodeDelayText = seconds + ? u"%1:%2"_q.arg(minutes).arg(seconds % 60, 2, 10, QChar('0')) + : QString(); + } + if (!state.starsToSend || state.type != Type::Send) { + _starsToSendText = Text::String(); + } else if (_starsToSendText.isEmpty() + || _state.starsToSend != state.starsToSend) { + _starsToSendText.setMarkedText( + _st.stars.style, + Text::IconEmoji(&st::starIconEmoji).append( + Lang::FormatCountToShort(state.starsToSend).string), + kMarkupTextOptions); + } + _state = state; + if (animate) { + _stateChangeFromWidth = width(); + _stateChangeAnimation.stop(); + updateSize(); _contentTo = grabContent(); - _a_typeChanged.start( - [=] { update(); }, + _stateChangeAnimation.start( + [=] { updateSize(); update(); }, 0., 1., st::universalDuration); - setPointerCursor(_type != Type::Slowmode); + setPointerCursor(_state.type != Type::Slowmode); + updateSize(); update(); } } -void SendButton::setSlowmodeDelay(int seconds) { - Expects(seconds >= 0 && seconds < kSlowmodeDelayLimit); - - if (_slowmodeDelay == seconds) { - return; - } - _slowmodeDelay = seconds; - _slowmodeDelayText = isSlowmode() - ? u"%1:%2"_q.arg(seconds / 60).arg(seconds % 60, 2, 10, QChar('0')) - : QString(); - setType(isSlowmode() ? Type::Slowmode : _afterSlowmodeType); - update(); -} - void SendButton::finishAnimating() { - _a_typeChanged.stop(); + _stateChangeAnimation.stop(); update(); } @@ -70,26 +84,60 @@ void SendButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); auto over = (isDown() || isOver()); - auto changed = _a_typeChanged.value(1.); + auto changed = _stateChangeAnimation.value(1.); if (changed < 1.) { PainterHighQualityEnabler hq(p); + const auto ratio = style::DevicePixelRatio(); + p.setOpacity(1. - changed); - auto targetRect = QRect((1 - kWideScale) / 2 * width(), (1 - kWideScale) / 2 * height(), kWideScale * width(), kWideScale * height()); - auto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * width(), changed); - auto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * height(), changed); - p.drawPixmap(targetRect.marginsAdded(QMargins(hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight)), _contentFrom); + const auto fromSize = _contentFrom.size() / (kWideScale * ratio); + const auto fromShift = QPoint( + (width() - fromSize.width()) / 2, + (height() - fromSize.height()) / 2); + auto fromRect = QRect( + (1 - kWideScale) / 2 * fromSize.width(), + (1 - kWideScale) / 2 * fromSize.height(), + kWideScale * fromSize.width(), + kWideScale * fromSize.height() + ).translated(fromShift); + auto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * fromSize.width(), changed); + auto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * fromSize.height(), changed); + p.drawPixmap( + fromRect.marginsAdded( + { hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight }), + _contentFrom); + p.setOpacity(changed); + const auto toSize = _contentTo.size() / (kWideScale * ratio); + const auto toShift = QPoint( + (width() - toSize.width()) / 2, + (height() - toSize.height()) / 2); + auto toRect = QRect( + (1 - kWideScale) / 2 * toSize.width(), + (1 - kWideScale) / 2 * toSize.height(), + kWideScale * toSize.width(), + kWideScale * toSize.height() + ).translated(toShift); auto shownWidth = anim::interpolate((1 - kWideScale) / 2 * width(), 0, changed); - auto shownHeight = anim::interpolate((1 - kWideScale) / 2 * height(), 0, changed); - p.drawPixmap(targetRect.marginsAdded(QMargins(shownWidth, shownHeight, shownWidth, shownHeight)), _contentTo); + auto shownHeight = anim::interpolate((1 - kWideScale) / 2 * toSize.height(), 0, changed); + p.drawPixmap( + toRect.marginsAdded( + { shownWidth, shownHeight, shownWidth, shownHeight }), + _contentTo); return; } - switch (_type) { + switch (_state.type) { case Type::Record: paintRecord(p, over); break; case Type::Round: paintRound(p, over); break; case Type::Save: paintSave(p, over); break; case Type::Cancel: paintCancel(p, over); break; - case Type::Send: paintSend(p, over); break; + case Type::Send: + if (_starsToSendText.isEmpty()) { + paintSend(p, over); + } else { + paintStarsToSend(p, over); + } + break; case Type::Schedule: paintSchedule(p, over); break; case Type::Slowmode: paintSlowmode(p); break; } @@ -152,6 +200,23 @@ void SendButton::paintSend(QPainter &p, bool over) { } } +void SendButton::paintStarsToSend(QPainter &p, bool over) { + const auto geometry = starsGeometry(); + { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(over ? _st.stars.textBgOver : _st.stars.textBg); + const auto radius = geometry.rounded.height() / 2; + p.drawRoundedRect(geometry.rounded, radius, radius); + } + p.setPen(over ? _st.stars.textFgOver : _st.stars.textFg); + _starsToSendText.draw(p, { + .position = geometry.inner.topLeft(), + .outerWidth = width(), + .availableWidth = geometry.inner.width(), + }); +} + void SendButton::paintSchedule(QPainter &p, bool over) { { PainterHighQualityEnabler hq(p); @@ -178,8 +243,40 @@ void SendButton::paintSlowmode(QPainter &p) { style::al_center); } -bool SendButton::isSlowmode() const { - return (_slowmodeDelay > 0); +SendButton::StarsGeometry SendButton::starsGeometry() const { + const auto &st = _st.stars; + const auto inner = QRect( + 0, + 0, + _starsToSendText.maxWidth(), + st.style.font->height); + const auto rounded = inner.marginsAdded(QMargins( + st.padding.left() - st.width / 2, + st.padding.top() + st.textTop, + st.padding.right() - st.width / 2, + st.height - st.padding.top() - st.textTop - st.style.font->height)); + const auto add = (_st.inner.height - rounded.height()) / 2; + const auto outer = rounded.marginsAdded(QMargins( + add, + add, + add, + _st.inner.height - add - rounded.height())); + const auto shift = -outer.topLeft(); + return { + .inner = inner.translated(shift), + .rounded = rounded.translated(shift), + .outer = outer.translated(shift), + }; +} + +void SendButton::updateSize() { + const auto finalWidth = _starsToSendText.isEmpty() + ? _st.inner.width + : starsGeometry().outer.width(); + const auto progress = _stateChangeAnimation.value(1.); + resize( + anim::interpolate(_stateChangeFromWidth, finalWidth, progress), + _st.inner.height); } QPixmap SendButton::grabContent() { @@ -195,7 +292,7 @@ QPixmap SendButton::grabContent() { (kWideScale - 1) / 2 * height(), GrabWidget(this)); } - return Ui::PixmapFromImage(std::move(result)); + return PixmapFromImage(std::move(result)); } QImage SendButton::prepareRippleMask() const { diff --git a/Telegram/SourceFiles/ui/controls/send_button.h b/Telegram/SourceFiles/ui/controls/send_button.h index 835974f51..b1483bbc4 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.h +++ b/Telegram/SourceFiles/ui/controls/send_button.h @@ -30,11 +30,21 @@ public: Cancel, Slowmode, }; + struct State { + Type type = Type::Send; + int slowmodeDelay = 0; + int starsToSend = 0; + + friend inline constexpr auto operator<=>(State, State) = default; + friend inline constexpr bool operator==(State, State) = default; + }; [[nodiscard]] Type type() const { - return _type; + return _state.type; } - void setType(Type state); - void setSlowmodeDelay(int seconds); + [[nodiscard]] State state() const { + return _state; + } + void setState(State state); void finishAnimating(); protected: @@ -44,8 +54,15 @@ protected: QPoint prepareRippleStartPosition() const override; private: + struct StarsGeometry { + QRect inner; + QRect rounded; + QRect outer; + }; [[nodiscard]] QPixmap grabContent(); - [[nodiscard]] bool isSlowmode() const; + void updateSize(); + + [[nodiscard]] StarsGeometry starsGeometry() const; void paintRecord(QPainter &p, bool over); void paintRound(QPainter &p, bool over); @@ -54,17 +71,18 @@ private: void paintSend(QPainter &p, bool over); void paintSchedule(QPainter &p, bool over); void paintSlowmode(QPainter &p); + void paintStarsToSend(QPainter &p, bool over); const style::SendButton &_st; - Type _type = Type::Send; - Type _afterSlowmodeType = Type::Send; + State _state; QPixmap _contentFrom, _contentTo; - Ui::Animations::Simple _a_typeChanged; + Ui::Animations::Simple _stateChangeAnimation; + int _stateChangeFromWidth = 0; - int _slowmodeDelay = 0; QString _slowmodeDelayText; + Ui::Text::String _starsToSendText; };