From 0441b7dbc35b37b2b0094f32c0d2ffa550c36cc1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 25 Nov 2025 16:46:33 +0400 Subject: [PATCH] Show star senders in live stories. --- .../boxes/star_gift_auction_box.cpp | 2 + Telegram/SourceFiles/calls/calls.style | 2 + .../calls/group/calls_group_messages.cpp | 51 +-- .../calls/group/calls_group_messages.h | 18 +- Telegram/SourceFiles/data/data_peer.cpp | 2 +- Telegram/SourceFiles/data/data_peer.h | 6 +- Telegram/SourceFiles/history/history.cpp | 1 + .../SourceFiles/history/history_widget.cpp | 2 +- .../view/controls/compose_controls_common.h | 5 + .../history_view_compose_controls.cpp | 296 +++++++++++++++++- .../controls/history_view_compose_controls.h | 16 +- .../view/history_view_paid_reaction_toast.cpp | 1 + .../stories/media_stories_controller.cpp | 17 +- .../media/stories/media_stories_controller.h | 5 + .../media/stories/media_stories_reply.cpp | 6 +- .../media/view/media_view_video_stream.cpp | 2 +- .../payments/payments_reaction_process.cpp | 3 +- .../payments/payments_reaction_process.h | 1 - Telegram/lib_ui | 2 +- 19 files changed, 373 insertions(+), 65 deletions(-) diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp index 6bd7ccbb49..22a1626e67 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -202,6 +202,8 @@ struct BidSliderValues { auto result = object_ptr(parent.get()); const auto raw = result.data(); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + struct State { std::unique_ptr place; std::unique_ptr userpic; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 9cb384513f..58d112d03e 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1721,6 +1721,8 @@ groupCallUserpicPadding: margins(2px, 2px, 4px, 2px); groupCallPinnedPadding: margins(10px, 4px, 10px, 2px); groupCallPinnedMaxWidth: 96px; groupCallPinnedUserpic: 22px; +groupCallEffectPadding: margins(3px, 1px, 3px, 1px); +groupCallEffectUserpicPadding: margins(1px, 1px, 3px, 1px); groupCallTopReactorBadge: RoundCheckbox(defaultRoundCheckbox) { width: 1px; diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages.cpp b/Telegram/SourceFiles/calls/group/calls_group_messages.cpp index dfcebb3695..9f35bc988f 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_messages.cpp @@ -69,12 +69,6 @@ constexpr auto kStarsStatsShortPollDelay = 30 * crl::time(1000); return PinFinishDate(message.peer, message.date, message.stars); } -[[nodiscard]] std::optional MaybeShownPeer( - uint32 privacySet, - PeerId shownPeer) { - return privacySet ? shownPeer : std::optional(); -} - } // namespace Messages::Messages(not_null call, not_null api) @@ -104,9 +98,7 @@ Messages::~Messages() { finishPaidSending({ .count = int(_paid.sending), .valid = true, - .shownPeer = MaybeShownPeer( - _paid.sendingPrivacySet, - _paid.sendingShownPeer), + .shownPeer = _paid.sendingShownPeer, }, false); } } @@ -517,26 +509,23 @@ PeerId Messages::reactionsLocalShownPeer() const { return entry.peer ? entry.peer->id : PeerId(); } } - return _session->userPeerId(); + return _call->messagesFrom()->id; //const auto api = &_session->api(); //return api->globalPrivacy().paidReactionShownPeerCurrent(); }; - return (_paid.scheduledFlag && _paid.scheduledPrivacySet) + return _paid.scheduledFlag ? _paid.scheduledShownPeer - : (_paid.sendingFlag && _paid.sendingPrivacySet) + : _paid.sendingFlag ? _paid.sendingShownPeer : minePaidShownPeer(); } -void Messages::reactionsPaidAdd(int count, std::optional shownPeer) { +void Messages::reactionsPaidAdd(int count) { Expects(count >= 0); _paid.scheduled += count; _paid.scheduledFlag = 1; - if (shownPeer.has_value()) { - _paid.scheduledShownPeer = *shownPeer; - _paid.scheduledPrivacySet = true; - } + _paid.scheduledShownPeer = _call->messagesFrom()->id; if (count > 0) { _session->credits().lock(CreditsAmount(count)); } @@ -555,7 +544,6 @@ void Messages::reactionsPaidScheduledCancel() { _paid.scheduled = 0; _paid.scheduledFlag = 0; _paid.scheduledShownPeer = 0; - _paid.scheduledPrivacySet = 0; _paidChanges.fire({}); } @@ -570,17 +558,13 @@ Data::PaidReactionSend Messages::startPaidReactionSending() { _paid.sending = _paid.scheduled; _paid.sendingFlag = _paid.scheduledFlag; _paid.sendingShownPeer = _paid.scheduledShownPeer; - _paid.sendingPrivacySet = _paid.scheduledPrivacySet; _paid.scheduled = 0; _paid.scheduledFlag = 0; _paid.scheduledShownPeer = 0; - _paid.scheduledPrivacySet = 0; return { .count = int(_paid.sending), .valid = true, - .shownPeer = MaybeShownPeer( - _paid.sendingPrivacySet, - _paid.sendingShownPeer), + .shownPeer = _paid.sendingShownPeer, }; } @@ -589,25 +573,24 @@ void Messages::finishPaidSending( bool success) { Expects(send.count == _paid.sending); Expects(send.valid == (_paid.sendingFlag == 1)); - Expects(send.shownPeer == MaybeShownPeer( - _paid.sendingPrivacySet, - _paid.sendingShownPeer)); + Expects(send.shownPeer == _paid.sendingShownPeer); _paid.sending = 0; _paid.sendingFlag = 0; _paid.sendingShownPeer = 0; - _paid.sendingPrivacySet = 0; if (const auto amount = send.count) { if (success) { + const auto from = _session->data().peer(*send.shownPeer); _session->credits().withdrawLocked(CreditsAmount(amount)); auto &donors = _paid.top.topDonors; - const auto i = ranges::find(donors, true, &StarsTopDonor::my); + const auto i = ranges::find(donors, true, &StarsDonor::my); if (i != end(donors)) { + i->peer = from; i->stars += amount; } else { donors.push_back({ - .peer = _session->user(), + .peer = from, .stars = amount, .my = true, }); @@ -632,7 +615,7 @@ void Messages::reactionsPaidSend() { const auto randomId = base::RandomValue(); _sendingIdByRandomId.emplace(randomId, localId); - const auto from = _call->messagesFrom(); + const auto from = _session->data().peer(*send.shownPeer); const auto stars = int(send.count); const auto skip = skipMessage({}, stars); if (skip) { @@ -676,7 +659,7 @@ void Messages::undoScheduledPaidOnDestroy() { Messages::PaidLocalState Messages::starsLocalState() const { const auto &donors = _paid.top.topDonors; - const auto i = ranges::find(donors, true, &StarsTopDonor::my); + const auto i = ranges::find(donors, true, &StarsDonor::my); const auto local = int(_paid.scheduled); const auto my = (i != end(donors) ? i->stars : 0) + local; const auto total = _paid.top.total + local; @@ -728,7 +711,7 @@ void Messages::addStars(not_null from, int stars, bool mine) { const auto i = ranges::find( _paid.top.topDonors, from.get(), - &StarsTopDonor::peer); + &StarsDonor::peer); if (i != end(_paid.top.topDonors)) { i->stars += stars; } else { @@ -741,8 +724,8 @@ void Messages::addStars(not_null from, int stars, bool mine) { ranges::stable_sort( _paid.top.topDonors, ranges::greater(), - &StarsTopDonor::stars); - _paidChanges.fire({}); + &StarsDonor::stars); + _paidChanges.fire({ .peer = from, .stars = stars }); } } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages.h b/Telegram/SourceFiles/calls/group/calls_group_messages.h index ff465c9735..9633ea2a25 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages.h +++ b/Telegram/SourceFiles/calls/group/calls_group_messages.h @@ -54,18 +54,18 @@ struct MessageDeleteRequest { bool reportSpam = false; }; -struct StarsTopDonor { +struct StarsDonor { PeerData *peer = nullptr; int stars = 0; bool my = false; friend inline bool operator==( - const StarsTopDonor &, - const StarsTopDonor &) = default; + const StarsDonor &, + const StarsDonor &) = default; }; struct StarsTop { - std::vector topDonors; + std::vector topDonors; int total = 0; friend inline bool operator==( @@ -91,7 +91,7 @@ public: [[nodiscard]] int reactionsPaidScheduled() const; [[nodiscard]] PeerId reactionsLocalShownPeer() const; - void reactionsPaidAdd(int count, std::optional shownPeer = {}); + void reactionsPaidAdd(int count); void reactionsPaidScheduledCancel(); void reactionsPaidSend(); void undoScheduledPaidOnDestroy(); @@ -101,7 +101,7 @@ public: int my = 0; }; [[nodiscard]] PaidLocalState starsLocalState() const; - [[nodiscard]] rpl::producer<> starsValueChanges() const { + [[nodiscard]] rpl::producer starsValueChanges() const { return _paidChanges.events(); } [[nodiscard]] const StarsTop &starsTop() const { @@ -155,7 +155,9 @@ private: const TextWithEntities &text, int stars) const; [[nodiscard]] Data::PaidReactionSend startPaidReactionSending(); - void finishPaidSending(Data::PaidReactionSend send, bool success); + void finishPaidSending( + Data::PaidReactionSend send, + bool success); void addStars(not_null from, int stars, bool mine); void requestStarsStats(); @@ -181,7 +183,7 @@ private: mtpRequestId _starsTopRequestId = 0; Paid _paid; - rpl::event_stream<> _paidChanges; + rpl::event_stream _paidChanges; bool _paidSendingPending = false; TimeId _ttl = 0; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index a81805ceaa..636cf43a97 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -449,7 +449,7 @@ QImage *PeerData::userpicCloudImage(Ui::PeerUserpicView &view) const { } void PeerData::paintUserpic( - Painter &p, + QPainter &p, Ui::PeerUserpicView &view, PaintUserpicContext context) const { if (const auto broadcast = monoforumBroadcast()) { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 6641b69ab1..ea21b562eb 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -387,11 +387,11 @@ public: void setUserpicPhoto(const MTPPhoto &data); void paintUserpic( - Painter &p, + QPainter &p, Ui::PeerUserpicView &view, PaintUserpicContext context) const; void paintUserpic( - Painter &p, + QPainter &p, Ui::PeerUserpicView &view, int x, int y, @@ -406,7 +406,7 @@ public: }); } void paintUserpicLeft( - Painter &p, + QPainter &p, Ui::PeerUserpicView &view, int x, int y, diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 07e7063d88..ac29821ab6 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" +#include "ui/painter.h" // remove when History::paintUserpic accepts QPainter #include "payments/payments_checkout_process.h" #include "core/crash_reports.h" #include "core/application.h" diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 63b706bed5..5ed8ea5964 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -307,7 +307,7 @@ HistoryWidget::HistoryWidget( , _paidReactionToast(std::make_unique( this, &session().data(), - rpl::single(st::topBarHeight), + rpl::single(0), [=](not_null view) { return _list && _list->itemTop(view) >= 0; })) diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 0771042b44..77e7a6d0b2 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -99,4 +99,9 @@ enum class ToggleCommentsState { WithNew, }; +struct SendStarButtonEffect { + not_null from; + int stars = 0; +}; + } // namespace HistoryView::Controls 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 5808549b26..708c7be30e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "base/platform/base_platform_info.h" #include "base/qt_signal_producer.h" +#include "base/random.h" #include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/edit_caption_box.h" @@ -50,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_chat_participants.h" #include "ui/boxes/confirm_box.h" +#include "ui/color_int_conversion.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "history/history.h" @@ -89,13 +91,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/silent_toggle.h" #include "ui/chat/choose_send_as.h" #include "ui/effects/spoiler_mess.h" +#include "ui/effects/reaction_fly_animation.h" #include "webrtc/webrtc_environment.h" #include "window/window_adaptive.h" #include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "mainwindow.h" +#include "styles/style_calls.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" #include "styles/style_menu_icons.h" namespace HistoryView { @@ -110,6 +115,12 @@ constexpr auto kMouseEvents = { QEvent::MouseButtonRelease }; constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200); +constexpr auto kMaxStarSendEffects = 4; +constexpr auto kMaxStarEffects = 4; +constexpr auto kStarEffectDuration = 2 * crl::time(1000); +constexpr auto kStarEffectRotationMax = 12; +constexpr auto kStarEffectScaleMin = 0.3; +constexpr auto kStarEffectScaleMax = 0.7; constexpr auto kCommonModifiers = 0 | Qt::ShiftModifier @@ -880,6 +891,87 @@ SendMenu::Details FieldHeader::saveMenuDetails(bool hasSendText) const { : SendMenu::Details(); } +struct ComposeControls::StarEffect { + StarEffect( + not_null canvas, + SendStarButtonEffect effect); + + Ui::ReactionFlyAnimation around; + Ui::PeerUserpicView userpic; + QImage badge; + not_null from; + crl::time start = 0; + float64 shift = 0.; + float64 progress = 0.; + int stars = 0; +}; + +ComposeControls::StarEffect::StarEffect( + not_null canvas, + SendStarButtonEffect effect) +: around( + &effect.from->owner().reactions(), + Ui::ReactionFlyAnimationArgs{ + .id = Data::ReactionId::Paid(), + .effectOnly = true, + }, + [canvas] { canvas->update(); }, + st::reactionInlineImage) +, from(effect.from) +, start(crl::now()) +, stars(effect.stars) { + auto price = Ui::Text::String( + st::whoReadDateStyle, + Ui::Text::IconEmoji( + &st::starIconEmojiSmall + ).append(Lang::FormatCountDecimal(stars)), + kMarkupTextOptions); + const auto padding = st::groupCallEffectPadding; + const auto priceHeight = st::whoReadDateStyle.font->height; + const auto priceTop = padding.top(); + const auto height = priceTop + priceHeight + padding.bottom(); + + const auto userpicPadding = st::groupCallEffectUserpicPadding; + const auto userpicSize = height + - userpicPadding.top() + - userpicPadding.bottom(); + + const auto leftSkip = userpicPadding.left() + + userpicSize + + userpicPadding.right(); + const auto widthSkip = leftSkip + padding.right(); + + const auto width = widthSkip + price.maxWidth(); + const auto priceLeft = leftSkip; + + const auto ratio = style::DevicePixelRatio(); + badge = QImage( + QSize(width, height) * ratio, + QImage::Format_ARGB32_Premultiplied); + badge.fill(Qt::transparent); + badge.setDevicePixelRatio(ratio); + + auto p = QPainter(&badge); + auto hq = PainterHighQualityEnabler(p); + const auto bg = Ui::ColorFromSerialized(StarsColoringForCount( + from->session().appConfig().groupCallColorings(), + stars).bgLight); + p.setPen(Qt::NoPen); + p.setBrush(bg); + p.drawRoundedRect(0, 0, width, height, height / 2., height / 2.); + from->paintUserpic(p, userpic, PaintUserpicContext{ + .position = QPoint(userpicPadding.left(), userpicPadding.top()), + .size = userpicSize, + .shape = Ui::PeerUserpicShape::Circle, + }); + p.setPen(st::white); + price.draw(p, { + .position = QPoint(priceLeft, priceTop), + .availableWidth = price.maxWidth(), + }); + shift = base::RandomIndex(360) / 360.; +} + ComposeControls::ComposeControls( not_null parent, ComposeControlsDescriptor descriptor) @@ -911,7 +1003,7 @@ ComposeControls::ComposeControls( ? _regularWindow->tabbedSelector() : not_null(_ownedSelector.get())) , _mode(descriptor.mode) -, _wrap(std::make_unique(parent)) +, _wrap(std::make_unique(_parent)) , _send(std::make_shared(_wrap.get(), _st.send)) , _like(_features.likes ? Ui::CreateChild(_wrap.get(), _st.like) @@ -944,7 +1036,7 @@ ComposeControls::ComposeControls( , _voiceRecordBar(std::make_unique( _wrap.get(), Controls::VoiceRecordBarDescriptor{ - .outerContainer = parent, + .outerContainer = _parent, .show = _show, .send = _send, .customCancelText = descriptor.voiceCustomCancelText, @@ -1334,7 +1426,8 @@ rpl::producer<> ComposeControls::commentsShownToggles() const { } void ComposeControls::setStarsReactionCounter( - rpl::producer count) { + rpl::producer count, + rpl::producer effects) { if (!count) { delete base::take(_starsReaction); updateControlsGeometry(_wrap->size()); @@ -1357,6 +1450,7 @@ void ComposeControls::setStarsReactionCounter( ) | rpl::start_with_next([=](Qt::MouseButton button) { if (button == Qt::LeftButton) { _starsReactionIncrements.fire({ .count = 1 }); + startStarsSendEffect(); } else { _show->show(Calls::Group::MakeVideoStreamStarsBox({ .show = _show, @@ -1374,9 +1468,201 @@ void ComposeControls::setStarsReactionCounter( } }, _starsReaction->lifetime()); + + std::move( + effects + ) | rpl::start_with_next([=](const SendStarButtonEffect &event) { + startStarsEffect(event); + }, _starsReaction->lifetime()); } } +void ComposeControls::startStarsSendEffect() { + if (!_starSendEffectsCanvas) { + setupStarsSendEffectsCanvas(); + } + while (_starSendEffects.size() >= kMaxStarSendEffects) { + _starSendEffects.erase(begin(_starSendEffects)); + } + _starSendEffects.push_back(std::make_unique( + &_show->session().data().reactions(), + Ui::ReactionFlyAnimationArgs{ + .id = Data::ReactionId::Paid(), + .effectOnly = true, + }, + [raw = _starSendEffectsCanvas.get()] { raw->update(); }, + st::reactionInlineImage)); +} + +void ComposeControls::setupStarsSendEffectsCanvas() { + _starSendEffectsCanvas = std::make_unique(_parent); + + const auto raw = _starSendEffectsCanvas.get(); + raw->show(); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto effectSize = st::reactionInlineImage * 2; + + rpl::combine( + _wrap->geometryValue(), + _writeRestricted->geometryValue(), + _starsReaction->geometryValue() + ) | rpl::start_with_next([=](QRect wrap, QRect restriction, QRect star) { + const auto parent = (_starsReaction->parentWidget() == _wrap.get()) + ? wrap + : restriction; + const auto adjusted = star.translated(parent.topLeft()); + + raw->setGeometry( + adjusted.x() + (adjusted.width() - effectSize) / 2, + adjusted.y() + (adjusted.height() - effectSize) / 2, + effectSize, + effectSize); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=] { + for (auto i = begin(_starSendEffects); i != end(_starSendEffects);) { + if ((*i)->finished()) { + i = _starSendEffects.erase(i); + } else { + ++i; + } + } + if (_starSendEffects.empty()) { + crl::on_main(raw, [=] { + if (_starSendEffectsCanvas.get() == raw + && _starSendEffects.empty()) { + _starSendEffectsCanvas = nullptr; + } + }); + return; + } + auto p = QPainter(raw); + const auto now = crl::now(); + const auto size = raw->width(); + const auto color = st::radialFg->c; + const auto skip = (size - st::reactionInlineImage) / 2; + const auto target = QRect( + QPoint(skip, skip), + QSize(st::reactionInlineImage, st::reactionInlineImage)); + for (const auto &animation : _starSendEffects) { + animation->paintGetArea(p, {}, target, color, {}, now); + } + }, raw->lifetime()); +} + +void ComposeControls::startStarsEffect(SendStarButtonEffect event) { + if (!_starEffectsCanvas) { + setupStarsEffectsCanvas(); + } + while (_starEffects.size() >= kMaxStarEffects) { + _starEffects.erase(begin(_starEffects)); + } + _starEffects.push_back(std::make_unique( + _starEffectsCanvas.get(), + event)); +} + +void ComposeControls::setupStarsEffectsCanvas() { + _starEffectsCanvas = std::make_unique(_parent); + + const auto raw = _starEffectsCanvas.get(); + raw->show(); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + + raw->lifetime().make_state([=] { + raw->update(); + })->start(); + + const auto effectSize = st::reactionInlineImage * 2; + const auto width = effectSize * 2; + const auto height = effectSize * 4; + + rpl::combine( + _wrap->geometryValue(), + _writeRestricted->geometryValue(), + _starsReaction->geometryValue() + ) | rpl::start_with_next([=](QRect wrap, QRect restriction, QRect star) { + const auto parent = (_starsReaction->parentWidget() == _wrap.get()) + ? wrap + : restriction; + const auto adjusted = star.translated(parent.topLeft()); + + raw->setGeometry( + adjusted.x() + (adjusted.width() - width) / 2, + adjusted.y() + adjusted.height() - height, + width, + height); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=] { + const auto now = crl::now(); + for (auto i = begin(_starEffects); i != end(_starEffects);) { + const auto progress = float64(now - (*i)->start) + / kStarEffectDuration; + if (progress >= 1.) { + i = _starEffects.erase(i); + } else { + (*i)->progress = progress; + ++i; + } + } + if (_starEffects.empty()) { + crl::on_main(raw, [=] { + if (_starEffectsCanvas.get() == raw + && _starEffects.empty()) { + _starEffectsCanvas = nullptr; + } + }); + return; + } + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + const auto color = st::radialFg->c; + const auto skip = (effectSize - st::reactionInlineImage) / 2; + for (const auto &animation : _starEffects) { + const auto progress = animation->progress; + const auto left = anim::interpolate( + 0, + width - effectSize, + animation->shift); + const auto top = anim::interpolate( + height - effectSize, + 0, + progress); + const auto opacity = (progress < 0.125) ? + (progress / 0.125) : + (progress > 0.875) ? + (1. - progress) / 0.125 + : 1.; + const auto scale = kStarEffectScaleMin + + (kStarEffectScaleMax - kStarEffectScaleMin) * opacity; + + const auto rotation = qSin(-M_PI_2 + + M_PI * (animation->shift + animation->progress) + ) * kStarEffectRotationMax; + const auto target = QRect( + QPoint(left + skip, top + skip), + QSize(st::reactionInlineImage, st::reactionInlineImage)); + const auto size = animation->badge.size() + / animation->badge.devicePixelRatio(); + const auto dx = (target.width() - size.width()) / 2; + const auto dy = (target.height() - size.height()) / 2; + + p.save(); + p.translate(target.center()); + p.rotate(rotation); + p.scale(scale, scale); + p.translate(-target.center()); + p.setOpacity(opacity); + p.drawImage(target.topLeft() + QPoint(dx, dy), animation->badge); + p.restore(); + + animation->around.paintGetArea(p, {}, target, color, {}, now); + } + }, raw->lifetime()); +} + void ComposeControls::setStarsReactionTop( rpl::producer> top) { _starsReactionTop = std::move(top); @@ -3025,9 +3311,9 @@ void ComposeControls::updateSendButtonType() { _send->setState({ .type = type, .fillBgOverride = (_chosenStarsCount.value_or(0) - ? StarsColoringForCount( + ? Ui::ColorFromSerialized(StarsColoringForCount( appConfig.groupCallColorings(), - *_chosenStarsCount).bgLight + *_chosenStarsCount).bgLight) : QColor()), .slowmodeDelay = delay, .starsToSend = shownStarsPerMessage(), 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 a1d6090aa1..1160075d84 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -70,6 +70,7 @@ class SilentToggle; class DropdownMenu; struct PreparedList; struct SendStarButtonState; +class ReactionFlyAnimation; } // namespace Ui namespace Ui::Emoji { @@ -143,6 +144,7 @@ public: using FieldHistoryAction = Ui::InputField::HistoryAction; using Mode = ComposeControlsMode; using ToggleCommentsState = Controls::ToggleCommentsState; + using SendStarButtonEffect = Controls::SendStarButtonEffect; ComposeControls( not_null parent, @@ -169,7 +171,8 @@ public: void setToggleCommentsButton(rpl::producer state); [[nodiscard]] rpl::producer<> commentsShownToggles() const; void setStarsReactionCounter( - rpl::producer count); + rpl::producer count, + rpl::producer effects); using StarReactionTop = Data::MessageReactionsTopPaid; void setStarsReactionTop( rpl::producer> top); @@ -278,6 +281,7 @@ public: [[nodiscard]] Ui::InputField *fieldForMention() const; private: + struct StarEffect; enum class TextUpdateEvent { SaveDraft = (1 << 0), SendTyping = (1 << 1), @@ -354,6 +358,10 @@ private: [[nodiscard]] bool hasSilentBroadcastToggle() const; [[nodiscard]] bool editStarsButtonShown() const; + void startStarsSendEffect(); + void setupStarsSendEffectsCanvas(); + void startStarsEffect(SendStarButtonEffect event); + void setupStarsEffectsCanvas(); // Look in the _field for the inline bot and query string. void updateInlineBotQuery(); @@ -389,7 +397,7 @@ private: const style::ComposeControls &_st; ChatHelpers::ComposeFeatures _features; - const not_null _parent; + const not_null _parent; const not_null _panelsParent; const std::shared_ptr _show; const not_null _session; @@ -428,6 +436,10 @@ private: Ui::RpWidget *_commentsShownNewDot = nullptr; Ui::IconButton *_attachToggle = nullptr; Ui::AbstractButton *_starsReaction = nullptr; + std::vector> _starSendEffects; + std::unique_ptr _starSendEffectsCanvas; + std::vector> _starEffects; + std::unique_ptr _starEffectsCanvas; std::unique_ptr _replaceMedia; const not_null _tabbedSelectorToggle; rpl::producer _fieldCustomPlaceholder; 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 ad5d75f904..762712192d 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp @@ -298,6 +298,7 @@ void PaidReactionToast::showFor( .padding = rpl::single(QMargins(leftSkip, 0, rightSkip, 0)), .st = &st, .attach = RectPart::Top, + .addToAttachSide = _topOffset.value(), .acceptinput = true, .infinite = true, }); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 5f01189363..055d259127 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -1779,13 +1779,18 @@ auto Controller::starsReactionsValue() const }); } +auto Controller::starsReactionsEffects() const +-> rpl::producer { + return _starsReactionEffects.events(); +} + void Controller::setStarsReactionIncrements(rpl::producer increments) { std::move( increments ) | rpl::start_with_next([=](int count) { if (const auto call = _videoStreamCall.get()) { const auto show = _delegate->storiesShow(); - Payments::TryAddingPaidReaction(call, count, std::nullopt, show); + Payments::TryAddingPaidReaction(call, count, show); } }, _videoStreamLifetime); } @@ -1925,9 +1930,15 @@ void Controller::updateVideoStream(not_null videoStream) { _commentsStateShowFromPinned, _videoStreamLifetime); - _starsReactions = rpl::single(rpl::empty) | rpl::then( + _starsReactions = rpl::single(Calls::Group::StarsDonor()) | rpl::then( videoStream->messages()->starsValueChanges() - ) | rpl::map([=] { + ) | rpl::map([=](const Calls::Group::StarsDonor &donor) { + if (const auto peer = donor.peer) { + _starsReactionEffects.fire({ + .from = peer, + .stars = donor.stars, + }); + } return videoStream->messages()->starsLocalState().total; }); _paidReactionToast->shownForCall( diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 1261995cc4..baf0d69c15 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -35,6 +35,7 @@ class DocumentMedia; namespace HistoryView::Controls { enum class ToggleCommentsState; +struct SendStarButtonEffect; } // namespace HistoryView::Controls namespace HistoryView::Reactions { @@ -86,6 +87,7 @@ struct RepostClickHandler; using CommentsState = HistoryView::Controls::ToggleCommentsState; using PaidReactionToast = HistoryView::PaidReactionToast; +using SendStarButtonEffect = HistoryView::Controls::SendStarButtonEffect; enum class HeaderLayout { Normal, @@ -186,6 +188,8 @@ public: void setCommentsShownToggles(rpl::producer<> toggles); [[nodiscard]] auto starsReactionsValue() const -> rpl::producer; + [[nodiscard]] auto starsReactionsEffects() const + -> rpl::producer; void setStarsReactionIncrements(rpl::producer increments); void unfocusReply(); @@ -354,6 +358,7 @@ private: MsgId _commentsLastId = 0; rpl::variable _starsReactions; rpl::variable _starsReactionHighlighted; + rpl::event_stream _starsReactionEffects; std::vector _cachedSourcesList; int _cachedSourceIndex = -1; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 6bc9653ddd..f88ca8a82c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -896,9 +896,9 @@ void ReplyArea::show( _controls->commentsShownToggles()); } using Controls = HistoryView::ComposeControls; - _controls->setStarsReactionCounter(stream - ? _controller->starsReactionsValue() - : nullptr); + _controls->setStarsReactionCounter( + stream ? _controller->starsReactionsValue() : nullptr, + stream ? _controller->starsReactionsEffects() : nullptr); _controller->setStarsReactionIncrements( _controls->starsReactionIncrements( ) | rpl::map([](Controls::StarReactionIncrement increment) { diff --git a/Telegram/SourceFiles/media/view/media_view_video_stream.cpp b/Telegram/SourceFiles/media/view/media_view_video_stream.cpp index 1b6567b584..a68607b41b 100644 --- a/Telegram/SourceFiles/media/view/media_view_video_stream.cpp +++ b/Telegram/SourceFiles/media/view/media_view_video_stream.cpp @@ -65,7 +65,7 @@ auto TopVideoStreamDonors(not_null call) -> rpl::producer> { const auto messages = call->messages(); return rpl::single(rpl::empty) | rpl::then( - messages->starsValueChanges() + messages->starsValueChanges() | rpl::to_empty ) | rpl::map([=] { const auto &list = messages->starsTop().topDonors; auto still = Ui::MaxTopPaidDonorsShown(); diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index 648b2259d5..d255033d73 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -124,7 +124,6 @@ void TryAddingPaidReaction( void TryAddingPaidReaction( not_null call, int count, - std::optional shownPeer, std::shared_ptr show, Fn finished) { const auto checkCall = [weak = base::make_weak(call), finished] { @@ -141,7 +140,7 @@ void TryAddingPaidReaction( if (result == Settings::SmallBalanceResult::Success || result == Settings::SmallBalanceResult::Already) { if (const auto call = checkCall()) { - call->messages()->reactionsPaidAdd(count, shownPeer); + call->messages()->reactionsPaidAdd(count); call->peer()->owner().notifyCallPaidReactionSent(call); if (const auto onstack = finished) { onstack(true); diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.h b/Telegram/SourceFiles/payments/payments_reaction_process.h index 9dea2b254f..1071b0dfbb 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.h +++ b/Telegram/SourceFiles/payments/payments_reaction_process.h @@ -42,7 +42,6 @@ void TryAddingPaidReaction( void TryAddingPaidReaction( not_null call, int count, - std::optional shownPeer, std::shared_ptr show, Fn finished = nullptr); diff --git a/Telegram/lib_ui b/Telegram/lib_ui index be555efdb2..0bd80915ed 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit be555efdb20de610f55fde6dc8f670e2114e26fd +Subproject commit 0bd80915edae932441858a46e7b2e3fda10970d9