Support custom send button for paid.

This commit is contained in:
John Preston 2025-02-27 17:34:20 +04:00
parent c6fd8bcb99
commit 17cf354c58
6 changed files with 215 additions and 60 deletions

View file

@ -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;

View file

@ -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

View file

@ -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<PeerData*> 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

View file

@ -524,6 +524,7 @@ public:
ListenWrap(
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st,
std::shared_ptr<Ui::SendButton> send,
not_null<Main::Session*> session,
not_null<Ui::RoundVideoResult*> data,
const style::font &font);
@ -551,6 +552,7 @@ private:
const not_null<Ui::RpWidget*> _parent;
const style::RecordBar &_st;
const std::shared_ptr<Ui::SendButton> _send;
const not_null<Main::Session*> _session;
const not_null<DocumentData*> _document;
const std::unique_ptr<VoiceData> _voiceData;
@ -585,11 +587,13 @@ private:
ListenWrap::ListenWrap(
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st,
std::shared_ptr<Ui::SendButton> send,
not_null<Main::Session*> session,
not_null<Ui::RoundVideoResult*> 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<ListenWrap>(
this,
_st,
_send,
&_show->session(),
&_data,
_cancelFont);
@ -2006,6 +2015,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
_listen = std::make_unique<ListenWrap>(
this,
_st,
_send,
&_show->session(),
&_data,
_cancelFont);

View file

@ -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 {

View file

@ -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;
};