From 52a199a3620da4128e5ef3c5ed6be81ee913a912 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 9 Aug 2024 15:19:02 +0200 Subject: [PATCH] Make nice paid reaction toast. --- .../view/history_view_paid_reaction_toast.cpp | 236 ++++++++++++++---- .../view/history_view_paid_reaction_toast.h | 10 +- Telegram/SourceFiles/ui/effects/premium.style | 9 + 3 files changed, 196 insertions(+), 59 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp index dcdf512ab..616d83b71 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp @@ -14,25 +14,126 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/view/history_view_element.h" #include "history/history_item.h" -//#include "main/main_session.h" #include "lang/lang_keys.h" +#include "ui/effects/numbers_animation.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/toast/toast_widget.h" #include "ui/widgets/buttons.h" -//#include "boxes/sticker_set_box.h" -//#include "boxes/premium_preview_box.h" +#include "ui/widgets/labels.h" +#include "ui/painter.h" #include "lottie/lottie_single_player.h" -//#include "window/window_session_controller.h" -//#include "settings/settings_premium.h" -//#include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_premium.h" namespace HistoryView { namespace { constexpr auto kPremiumToastDuration = 5 * crl::time(1000); +[[nodiscard]] not_null MakeUndoButton( + not_null parent, + int width, + const QString &text, + rpl::producer finish, + crl::time total, + Fn click, + Fn timeout) { + const auto result = Ui::CreateChild(parent); + result->setClickedCallback(std::move(click)); + + struct State { + explicit State(not_null button) + : countdown( + st::toastUndoFont, + [=] { button->update(); }) { + } + + Ui::NumbersAnimation countdown; + crl::time finish = 0; + int secondsLeft = 0; + Ui::Animations::Basic animation; + Fn update; + base::Timer timer; + }; + const auto state = result->lifetime().make_state(result); + const auto updateLeft = [=] { + const auto now = crl::now(); + const auto left = state->finish - now; + if (left > 0) { + const auto seconds = int((left + 999) / 1000); + if (state->secondsLeft != seconds) { + state->secondsLeft = seconds; + state->countdown.setText(QString::number(seconds), seconds); + } + state->timer.callOnce((left % 1000) + 1); + } else { + state->animation.stop(); + state->timer.cancel(); + timeout(); + } + if (anim::Disabled()) { + } + }; + state->update = [=] { + if (anim::Disabled()) { + state->animation.stop(); + } else { + if (!state->animation.animating()) { + state->animation.start(); + } + state->timer.cancel(); + } + updateLeft(); + result->update(); + }; + + result->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(result); + + const auto font = st::historyPremiumViewSet.font; + const auto top = (result->height() - font->height) / 2; + auto pen = st::historyPremiumViewSet.textFg->p; + p.setPen(pen); + p.setFont(font); + p.drawText(0, top + font->ascent, text); + + const auto inner = QRect( + width - st::toastUndoSkip - st::toastUndoDiameter, + (result->height() - st::toastUndoDiameter) / 2, + st::toastUndoDiameter, + st::toastUndoDiameter); + p.setFont(st::toastUndoFont); + state->countdown.paint( + p, + inner.x() + (inner.width() - state->countdown.countWidth()) / 2, + inner.y() + (inner.height() - st::toastUndoFont->height) / 2, + width); + + const auto progress = (state->finish - crl::now()) / float64(total); + const auto len = int(base::SafeRound(arc::kFullLength * progress)); + if (len > 0) { + const auto from = arc::kFullLength / 4; + auto hq = PainterHighQualityEnabler(p); + pen.setWidthF(st::toastUndoStroke); + p.setPen(pen); + p.drawArc(inner, from, len); + } + }, result->lifetime()); + result->resize(width, st::historyPremiumViewSet.height); + + std::move(finish) | rpl::start_with_next([=](crl::time value) { + state->finish = value; + state->update(); + }, result->lifetime()); + state->animation.init(state->update); + state->timer.setCallback(state->update); + state->update(); + + result->show(); + return result; +} + } // namespace PaidReactionToast::PaidReactionToast( @@ -60,30 +161,32 @@ PaidReactionToast::~PaidReactionToast() { } } -void PaidReactionToast::maybeShowFor(not_null item) { +bool PaidReactionToast::maybeShowFor(not_null item) { const auto count = item->reactionsPaidScheduled(); const auto at = _owner->reactions().sendingScheduledPaidAt(item); if (!count || !at) { - return; + return false; } - const auto left = at - crl::now(); const auto total = Data::Reactions::ScheduledPaidDelay(); const auto ignore = total % 1000; - if (left > ignore) { - showFor(item->fullId(), count, left - ignore, total); + if (at <= crl::now() + ignore) { + return false; } + showFor(item->fullId(), count, at - ignore, total); + return true; } void PaidReactionToast::showFor( FullMsgId itemId, int count, - crl::time left, + crl::time finish, crl::time total) { const auto old = _weak.get(); const auto i = ranges::find(_stack, itemId); if (i != end(_stack)) { if (old && i + 1 == end(_stack)) { - update(old, count, left, total); + _count = count; + _timeFinish = finish; return; } _stack.erase(i); @@ -93,28 +196,47 @@ void PaidReactionToast::showFor( clearHiddenHiding(); if (old) { old->hideAnimated(); - _hiding.push_back(_weak); + _hiding.push_back(base::take(_weak)); } - const auto text = tr::lng_paid_react_toast_title( - tr::now, - Ui::Text::Bold - ).append('\n').append(tr::lng_paid_react_toast_text( - tr::now, - lt_count, - count, - Ui::Text::RichLangValue - )); - _st = st::historyPremiumToast; - const auto skip = _st.padding.top(); - const auto size = _st.style.font->height * 2; - const auto undo = tr::lng_paid_react_undo(tr::now); - _st.padding.setLeft(skip + size + skip); - _st.padding.setRight(st::historyPremiumViewSet.font->width(undo) - - st::historyPremiumViewSet.width); + _count.reset(); + _timeFinish.reset(); + _count = count; + _timeFinish = finish; + auto text = rpl::combine( + tr::lng_paid_react_toast_title(Ui::Text::Bold), + tr::lng_paid_react_toast_text( + lt_count_decimal, + _count.value() | tr::to_count(), + Ui::Text::RichLangValue) + ) | rpl::map([](TextWithEntities &&title, TextWithEntities &&body) { + title.append('\n').append(body); + return std::move(title); + }); + const auto &st = st::historyPremiumToast; + const auto skip = st.padding.top(); + const auto size = st.style.font->height * 2; + const auto undoText = tr::lng_paid_react_undo(tr::now); + auto content = object_ptr((QWidget*)nullptr); + const auto child = Ui::CreateChild( + content.data(), + std::move(text), + st::paidReactToastLabel); + content->resize(child->naturalWidth() * 1.5, child->height()); + child->show(); + + const auto leftSkip = skip + size + skip - st.padding.left(); + const auto undoFont = st::historyPremiumViewSet.font; + + const auto rightSkip = undoFont->width(undoText) + + st::toastUndoSpace + + st::toastUndoDiameter + + st::toastUndoSkip + - st.padding.right(); _weak = Ui::Toast::Show(_parent, Ui::Toast::Config{ - .text = text, - .st = &_st, + .content = std::move(content), + .padding = rpl::single(QMargins(leftSkip, 0, rightSkip, 0)), + .st = &st, .attach = RectPart::Top, .acceptinput = true, .infinite = true, @@ -124,18 +246,41 @@ void PaidReactionToast::showFor( return; } const auto widget = strong->widget(); - const auto hideToast = [weak = _weak] { + const auto hideToast = [=, weak = _weak] { if (const auto strong = weak.get()) { - strong->hideAnimated(); + if (strong == _weak.get()) { + _stack.erase(ranges::remove(_stack, itemId), end(_stack)); + + _hiding.push_back(base::take(_weak)); + strong->hideAnimated(); + + while (!_stack.empty()) { + if (const auto item = _owner->message(_stack.back())) { + if (maybeShowFor(item)) { + break; + } + } + _stack.pop_back(); + } + } } }; - const auto button = Ui::CreateChild( + const auto undo = [=] { + if (const auto item = _owner->message(itemId)) { + _owner->reactions().undoScheduledPaid(item); + } + hideToast(); + }; + const auto button = MakeUndoButton( widget.get(), - rpl::single(undo), - st::historyPremiumViewSet); - button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - button->show(); + rightSkip + st.padding.right(), + undoText, + _timeFinish.value(), + total, + undo, + hideToast); + rpl::combine( widget->sizeValue(), button->sizeValue() @@ -151,19 +296,6 @@ void PaidReactionToast::showFor( preview->show(); setupLottiePreview(preview, size); - button->setClickedCallback([=] { - if (const auto item = _owner->message(itemId)) { - _owner->reactions().undoScheduledPaid(item); - } - hideToast(); - }); -} - -void PaidReactionToast::update( - not_null toast, - int count, - crl::time left, - crl::time total) { } void PaidReactionToast::clearHiddenHiding() { diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h index 5b42ad806..593fc0208 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h @@ -36,17 +36,12 @@ public: ~PaidReactionToast(); private: - void maybeShowFor(not_null item); + bool maybeShowFor(not_null item); void showFor( FullMsgId itemId, int count, crl::time left, crl::time total); - void update( - not_null toast, - int count, - crl::time left, - crl::time total); void setupLottiePreview(not_null widget, int size); void clearHiddenHiding(); @@ -55,9 +50,10 @@ private: const not_null _owner; const rpl::variable _topOffset; - style::Toast _st; base::weak_ptr _weak; std::vector> _hiding; + rpl::variable _count; + rpl::variable _timeFinish; std::vector _stack; diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index ba85887ff..d13977148 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -394,3 +394,12 @@ paidReactTopMargin: margins(0px, 12px, 0px, 11px); paidReactTopUserpic: 42px; paidReactTopNameSkip: 47px; paidReactTopBadgeSkip: 32px; +paidReactToastLabel: FlatLabel(defaultFlatLabel) { + textFg: toastFg; + palette: defaultToastPalette; +} +toastUndoStroke: 2px; +toastUndoSpace: 8px; +toastUndoDiameter: 20px; +toastUndoSkip: 20px; +toastUndoFont: font(12px bold);