diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 399e0c5bdf..b3c7bf2686 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3055,6 +3055,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_small_balance_title#other" = "{count} Stars Needed"; "lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps."; "lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts."; +"lng_credits_small_balance_video_stream" = "Buy **Stars** to send them to {name} to support their stream."; "lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels."; "lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts."; "lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}."; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 26f14f233b..43a4393db2 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -750,6 +750,7 @@ GroupCall::~GroupCall() { if (!_rtmp) { Core::App().mediaDevices().setCaptureMuteTracker(this, false); } + _messages->undoScheduledPaidOnDestroy(); } void GroupCall::initConferenceE2E() { diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages.cpp b/Telegram/SourceFiles/calls/group/calls_group_messages.cpp index 10a34f8c26..c252a5e064 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_messages.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_group_call.h" #include "calls/group/calls_group_message_encryption.h" #include "data/data_group_call.h" +#include "data/data_message_reactions.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_user.h" @@ -47,13 +48,20 @@ namespace { return result; } +[[nodiscard]] std::optional MaybeShownPeer( + uint32 privacySet, + PeerId shownPeer) { + return privacySet ? shownPeer : std::optional(); +} + } // namespace Messages::Messages(not_null call, not_null api) : _call(call) +, _session(&call->peer()->session()) , _api(api) , _destroyTimer([=] { checkDestroying(); }) -, _ttl(_call->peer()->session().appConfig().groupCallMessageTTL()) { +, _ttl(_session->appConfig().groupCallMessageTTL()) { Ui::PostponeCall(_call, [=] { _call->real( ) | rpl::start_with_next([=](not_null call) { @@ -74,16 +82,30 @@ Messages::Messages(not_null call, not_null api) )).done([=](const MTPphone_GroupCallStars &result) { const auto &data = result.data(); - const auto owner = &_call->peer()->owner(); + const auto owner = &_session->data(); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); - _starsTop = ParseStarsTop(owner, result); + _paid.top = ParseStarsTop(owner, result); + _paidChanges.fire({}); }).send(); }, _lifetime); }); } +Messages::~Messages() { + if (_paid.sending > 0) { + finishPaidSending({ + .count = int(_paid.sending), + .valid = true, + .shownPeer = MaybeShownPeer( + _paid.sendingPrivacySet, + _paid.sendingShownPeer), + }, false); + } + +} + bool Messages::ready() const { return _real && (!_call->conference() || _call->e2eEncryptDecrypt()); } @@ -128,7 +150,7 @@ void Messages::send(TextWithTags text, int stars) { )).done([=]( const MTPUpdates &result, const MTP::Response &response) { - _call->peer()->session().api().applyUpdates(result, randomId); + _session->api().applyUpdates(result, randomId); }).fail([=](const MTP::Error &, const MTP::Response &response) { failed(randomId, response); }).send(); @@ -350,6 +372,9 @@ void Messages::sendPending() { for (auto &pending : base::take(_pending)) { send(std::move(pending.text), pending.stars); } + if (_paidSendingPending) { + reactionsPaidSend(); + } } void Messages::pushChanges() { @@ -379,4 +404,170 @@ void Messages::failed(uint64 randomId, const MTP::Response &response) { } } +int Messages::reactionsPaidScheduled() const { + return _paid.scheduled; +} + +PeerId Messages::reactionsLocalShownPeer() const { + const auto minePaidShownPeer = [&] { + for (const auto &entry : _paid.top.topDonors) { + if (entry.my) { + return entry.peer ? entry.peer->id : PeerId(); + } + } + return _session->userPeerId(); + //const auto api = &_session->api(); + //return api->globalPrivacy().paidReactionShownPeerCurrent(); + }; + return (_paid.scheduledFlag && _paid.scheduledPrivacySet) + ? _paid.scheduledShownPeer + : (_paid.sendingFlag && _paid.sendingPrivacySet) + ? _paid.sendingShownPeer + : minePaidShownPeer(); +} + +void Messages::reactionsPaidAdd(int count, std::optional shownPeer) { + Expects(count >= 0); + + _paid.scheduled += count; + _paid.scheduledFlag = 1; + if (shownPeer.has_value()) { + _paid.scheduledShownPeer = *shownPeer; + _paid.scheduledPrivacySet = true; + } + if (count > 0) { + _session->credits().lock(CreditsAmount(count)); + } + _call->peer()->owner().reactions().schedulePaid(_call); + _paidChanges.fire({}); +} + +void Messages::reactionsPaidScheduledCancel() { + if (!_paid.scheduledFlag) { + return; + } + if (const auto amount = int(_paid.scheduled)) { + _session->credits().unlock( + CreditsAmount(amount)); + } + _paid.scheduled = 0; + _paid.scheduledFlag = 0; + _paid.scheduledShownPeer = 0; + _paid.scheduledPrivacySet = 0; + _paidChanges.fire({}); +} + +Data::PaidReactionSend Messages::startPaidReactionSending() { + _paidSendingPending = false; + if (!_paid.scheduledFlag || !_paid.scheduled) { + return {}; + } else if (_paid.sendingFlag || !ready()) { + _paidSendingPending = true; + return {}; + } + _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), + }; +} + +void Messages::finishPaidSending( + Data::PaidReactionSend send, + bool success) { + Expects(send.count == _paid.sending); + Expects(send.valid == (_paid.sendingFlag == 1)); + Expects(send.shownPeer == MaybeShownPeer( + _paid.sendingPrivacySet, + _paid.sendingShownPeer)); + + _paid.sending = 0; + _paid.sendingFlag = 0; + _paid.sendingShownPeer = 0; + _paid.sendingPrivacySet = 0; + if (const auto amount = send.count) { + if (success) { + _session->credits().withdrawLocked(CreditsAmount(amount)); + + auto &donors = _paid.top.topDonors; + const auto i = ranges::find(donors, true, &StarsTopDonor::my); + if (i != end(donors)) { + i->stars += amount; + } else { + donors.push_back({ + .peer = _session->user(), + .stars = amount, + .my = true, + }); + } + } else { + _session->credits().unlock(CreditsAmount(amount)); + _paidChanges.fire({}); + } + } + if (_paidSendingPending) { + reactionsPaidSend(); + } +} + +void Messages::reactionsPaidSend() { + const auto send = startPaidReactionSending(); + if (!send.valid || !send.count) { + return; + } + + const auto localId = _call->peer()->owner().nextLocalMessageId(); + const auto randomId = base::RandomValue(); + _sendingIdByRandomId.emplace(randomId, localId); + + const auto from = _call->messagesFrom(); // send.shownPeer? + _messages.push_back({ + .id = localId, + .peer = from, + .stars = int(send.count), + }); + + using Flag = MTPphone_SendGroupCallMessage::Flag; + _api->request(MTPphone_SendGroupCallMessage( + MTP_flags(Flag::f_allow_paid_stars), + _call->inputCall(), + MTP_long(randomId), + MTP_textWithEntities(MTP_string(), MTP_vector()), + MTP_long(send.count) + )).done([=]( + const MTPUpdates &result, + const MTP::Response &response) { + finishPaidSending(send, true); + _session->api().applyUpdates(result, randomId); + }).fail([=](const MTP::Error &, const MTP::Response &response) { + finishPaidSending(send, false); + failed(randomId, response); + }).send(); + + checkDestroying(true); +} + +void Messages::undoScheduledPaidOnDestroy() { + _call->peer()->owner().reactions().undoScheduledPaid(_call); +} + +Messages::PaidLocalState Messages::starsLocalState() const { + const auto &donors = _paid.top.topDonors; + const auto i = ranges::find(donors, true, &StarsTopDonor::my); + const auto local = int(_paid.scheduled + _paid.sending); + const auto my = (i != end(donors) ? i->stars : 0) + local; + const auto total = _paid.top.total + local; + return { .total = total, .my = my }; +} + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages.h b/Telegram/SourceFiles/calls/group/calls_group_messages.h index 15eb7dd441..baeb543a8d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages.h +++ b/Telegram/SourceFiles/calls/group/calls_group_messages.h @@ -16,8 +16,13 @@ class GroupCall; namespace Data { class GroupCall; +struct PaidReactionSend; } // namespace Data +namespace Main { +class Session; +} // namespace Main + namespace MTP { class Sender; struct Response; @@ -61,6 +66,7 @@ struct StarsTop { class Messages final : public base::has_weak_ptr { public: Messages(not_null call, not_null api); + ~Messages(); void send(TextWithTags text, int stars); @@ -72,8 +78,23 @@ public: [[nodiscard]] rpl::producer> listValue() const; [[nodiscard]] rpl::producer idUpdates() const; - [[nodiscard]] rpl::producer starsTopValue() const { - return _starsTop.value(); + [[nodiscard]] int reactionsPaidScheduled() const; + [[nodiscard]] PeerId reactionsLocalShownPeer() const; + void reactionsPaidAdd(int count, std::optional shownPeer = {}); + void reactionsPaidScheduledCancel(); + void reactionsPaidSend(); + void undoScheduledPaidOnDestroy(); + + struct PaidLocalState { + int total = 0; + int my = 0; + }; + [[nodiscard]] PaidLocalState starsLocalState() const; + [[nodiscard]] rpl::producer<> starsValueChanges() const { + return _paidChanges.events(); + } + [[nodiscard]] const StarsTop &starsTop() const { + return _paid.top; } private: @@ -81,6 +102,18 @@ private: TextWithTags text; int stars = 0; }; + struct Paid { + StarsTop top; + PeerId scheduledShownPeer = 0; + PeerId sendingShownPeer = 0; + uint32 scheduled : 30 = 0; + uint32 scheduledFlag : 1 = 0; + uint32 scheduledPrivacySet : 1 = 0; + uint32 sending : 30 = 0; + uint32 sendingFlag : 1 = 0; + uint32 sendingPrivacySet : 1 = 0; + }; + [[nodiscard]] bool ready() const; void sendPending(); void pushChanges(); @@ -97,7 +130,11 @@ private: void sent(uint64 randomId, MsgId realId); void failed(uint64 randomId, const MTP::Response &response); + [[nodiscard]] Data::PaidReactionSend startPaidReactionSending(); + void finishPaidSending(Data::PaidReactionSend send, bool success); + const not_null _call; + const not_null _session; const not_null _api; MsgId _conferenceIdAutoIncrement = 0; @@ -115,7 +152,9 @@ private: rpl::event_stream _idUpdates; mtpRequestId _starsTopRequestId = 0; - rpl::variable _starsTop; + Paid _paid; + rpl::event_stream<> _paidChanges; + bool _paidSendingPending = false; TimeId _ttl = 0; bool _changesScheduled = false; diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 96e3d303ea..1b8b31c21c 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_message_reactions.h" #include "api/api_global_privacy.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_messages.h" #include "chat_helpers/stickers_lottie.h" #include "core/application.h" #include "history/history.h" @@ -1661,6 +1663,24 @@ crl::time Reactions::sendingScheduledPaidAt( return (i != end(_sendPaidItems)) ? i->second : crl::time(); } +void Reactions::schedulePaid(not_null call) { + _sendPaidCalls[call] = crl::now() + kPaidAccumulatePeriod; + if (!_sendPaidTimer.isActive()) { + _sendPaidTimer.callOnce(kPaidAccumulatePeriod); + } +} + +void Reactions::undoScheduledPaid(not_null call) { + _sendPaidCalls.remove(call); + call->messages()->reactionsPaidScheduledCancel(); +} + +crl::time Reactions::sendingScheduledPaidAt( + not_null call) const { + const auto i = _sendPaidCalls.find(call); + return (i != end(_sendPaidCalls)) ? i->second : crl::time(); +} + crl::time Reactions::ScheduledPaidDelay() { return kPaidAccumulatePeriod; } @@ -1754,13 +1774,27 @@ void Reactions::CheckUnknownForUnread( } void Reactions::sendPaid() { - if (!_sendingPaid.empty()) { - return; - } auto next = crl::time(); const auto now = crl::now(); - for (auto i = begin(_sendPaidItems); i != end(_sendPaidItems);) { - const auto item = i->first; + if (_sendingPaid.empty()) { + for (auto i = begin(_sendPaidItems); i != end(_sendPaidItems);) { + const auto item = i->first; + const auto when = i->second; + if (when > now) { + if (!next || next > when) { + next = when; + } + ++i; + } else { + i = _sendPaidItems.erase(i); + if (sendPaid(item)) { + return; + } + } + } + } + for (auto i = begin(_sendPaidCalls); i != end(_sendPaidCalls);) { + const auto call = i->first; const auto when = i->second; if (when > now) { if (!next || next > when) { @@ -1768,10 +1802,8 @@ void Reactions::sendPaid() { } ++i; } else { - i = _sendPaidItems.erase(i); - if (sendPaid(item)) { - return; - } + i = _sendPaidCalls.erase(i); + call->messages()->reactionsPaidSend(); } } if (next) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index da0d46d6ac..11da1fec9a 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_message_reaction_id.h" #include "data/stickers/data_custom_emoji.h" +namespace Calls { +class GroupCall; +} // namespace Calls + namespace Ui { class AnimatedIcon; } // namespace Ui @@ -154,10 +158,17 @@ public: SavedSublist *sublist = nullptr); [[nodiscard]] bool isQuitPrevent(); + void schedulePaid(not_null item); void undoScheduledPaid(not_null item); [[nodiscard]] crl::time sendingScheduledPaidAt( not_null item) const; + + void schedulePaid(not_null call); + void undoScheduledPaid(not_null call); + [[nodiscard]] crl::time sendingScheduledPaidAt( + not_null call) const; + [[nodiscard]] static crl::time ScheduledPaidDelay(); [[nodiscard]] static bool HasUnread(const MTPMessageReactions &data); @@ -355,6 +366,8 @@ private: base::flat_map, mtpRequestId> _sendingPaid; base::Timer _sendPaidTimer; + base::flat_map, crl::time> _sendPaidCalls; + mtpRequestId _saveFaveRequestId = 0; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 34f946f9ac..04946f1533 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -2138,6 +2138,14 @@ rpl::producer> Session::viewPaidReactionSent() cons return _viewPaidReactionSent.events(); } +void Session::notifyCallPaidReactionSent(not_null call) { + _callPaidReactionSent.fire_copy(call); +} + +rpl::producer> Session::callPaidReactionSent() const { + return _callPaidReactionSent.events(); +} + void Session::notifyHistoryUnloaded(not_null history) { _historyUnloaded.fire_copy(history); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 37a55feb98..6b52735d3a 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -22,6 +22,10 @@ struct WebPageStickerSet; enum class WebPageType : uint8; enum class NewMessageType; +namespace Calls { +class GroupCall; +} // namespace Calls + namespace HistoryView { struct Group; class Element; @@ -385,6 +389,8 @@ public: [[nodiscard]] rpl::producer> historyChanged() const; void notifyViewPaidReactionSent(not_null view); [[nodiscard]] rpl::producer> viewPaidReactionSent() const; + void notifyCallPaidReactionSent(not_null call); + [[nodiscard]] rpl::producer> callPaidReactionSent() const; void sendHistoryChangeNotifications(); void notifyPinnedDialogsOrderUpdated(); @@ -1079,6 +1085,7 @@ private: rpl::event_stream> _itemRemoved; rpl::event_stream> _viewRemoved; rpl::event_stream> _viewPaidReactionSent; + rpl::event_stream> _callPaidReactionSent; rpl::event_stream> _historyUnloaded; rpl::event_stream> _historyCleared; base::flat_set> _historiesChanged; 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 2fd79450bc..4c1374d0c0 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp @@ -7,10 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_paid_reaction_toast.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_messages.h" #include "chat_helpers/stickers_lottie.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_message_reactions.h" +#include "data/data_peer.h" #include "data/data_session.h" #include "history/view/history_view_element.h" #include "history/history_item.h" @@ -152,6 +155,22 @@ PaidReactionToast::PaidReactionToast( }, _lifetime); } +PaidReactionToast::PaidReactionToast( + not_null parent, + not_null owner, + rpl::producer topOffset, + Fn call)> mine) +: _parent(parent) +, _owner(owner) +, _topOffset(std::move(topOffset)) { + _owner->callPaidReactionSent( + ) | rpl::filter( + std::move(mine) + ) | rpl::start_with_next([=](not_null call) { + maybeShowFor(call); + }, _lifetime); +} + PaidReactionToast::~PaidReactionToast() { _hiding.push_back(_weak); for (const auto &weak : base::take(_hiding)) { @@ -177,6 +196,22 @@ bool PaidReactionToast::maybeShowFor(not_null item) { return true; } +bool PaidReactionToast::maybeShowFor(not_null call) { + const auto count = call->messages()->reactionsPaidScheduled(); + const auto shownPeer = call->messagesFrom()->id; + const auto at = _owner->reactions().sendingScheduledPaidAt(call); + if (!count || !at) { + return false; + } + const auto total = Data::Reactions::ScheduledPaidDelay(); + const auto ignore = total % 1000; + if (at <= crl::now() + ignore) { + return false; + } + showFor(idForCall(call), count, shownPeer, at - ignore, total); + return true; +} + void PaidReactionToast::showFor( FullMsgId itemId, int count, @@ -281,7 +316,12 @@ void PaidReactionToast::showFor( }; const auto undo = [=] { - if (const auto item = _owner->message(itemId)) { + const auto i = _idsForCalls.find(itemId); + if (i != end(_idsForCalls)) { + if (const auto strong = i->second.get()) { + _owner->reactions().undoScheduledPaid(strong); + } + } else if (const auto item = _owner->message(itemId)) { _owner->reactions().undoScheduledPaid(item); } hideToast(); @@ -312,6 +352,24 @@ void PaidReactionToast::showFor( setupLottiePreview(preview, size); } +FullMsgId PaidReactionToast::idForCall(not_null call) { + for (auto i = begin(_idsForCalls); i != end(_idsForCalls);) { + const auto strong = i->second.get(); + if (strong == call) { + return i->first; + } else if (!strong) { + i = _idsForCalls.erase(i); + } else { + ++i; + } + } + const auto itemId = FullMsgId( + call->peer()->id, + call->peer()->owner().nextLocalMessageId()); + _idsForCalls.emplace(itemId, call); + return itemId; +} + void PaidReactionToast::clearHiddenHiding() { _hiding.erase( ranges::remove( 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 485778e209..c3727edfe4 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_widgets.h" +namespace Calls { +class GroupCall; +} // namespace Calls + namespace Data { class Session; } // namespace Data @@ -33,10 +37,16 @@ public: not_null owner, rpl::producer topOffset, Fn view)> mine); + PaidReactionToast( + not_null parent, + not_null owner, + rpl::producer topOffset, + Fn call)> mine); ~PaidReactionToast(); private: bool maybeShowFor(not_null item); + bool maybeShowFor(not_null call); void showFor( FullMsgId itemId, int count, @@ -44,6 +54,8 @@ private: crl::time left, crl::time total); + [[nodiscard]] FullMsgId idForCall(not_null call); + void setupLottiePreview(not_null widget, int size); void clearHiddenHiding(); @@ -59,6 +71,8 @@ private: std::vector _stack; + base::flat_map> _idsForCalls; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index e371708262..ab9fcf6743 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -27,8 +27,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_stories.h" -#include "history/view/reactions/history_view_reactions_strip.h" #include "history/view/controls/compose_controls_common.h" +#include "history/view/reactions/history_view_reactions_strip.h" +#include "history/view/history_view_paid_reaction_toast.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "media/stories/media_stories_caption_full_view.h" @@ -45,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" #include "info/stories/info_stories_common.h" +#include "payments/payments_reaction_process.h" #include "settings/settings_credits_graphics.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/report_box_graphics.h" @@ -301,6 +303,13 @@ Controller::Controller(not_null delegate) , _replyArea(std::make_unique(this)) , _reactions(std::make_unique(this)) , _recentViews(std::make_unique(this)) +, _paidReactionToast(std::make_unique( + _wrap, + &delegate->storiesShow()->session().data(), + paidReactionToastTopValue(), + [=](not_null call) { + return _videoStreamCall.get() == call; + })) , _weatherInCelsius(ResolveWeatherInCelsius()){ initLayout(); @@ -664,6 +673,18 @@ bool Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) { return result; } +rpl::producer Controller::paidReactionToastTopValue() const { + return _layout.value( + ) | rpl::map([](const std::optional &layout) { + const auto base = !layout + ? 0 + : (layout->headerLayout == HeaderLayout::Normal) + ? (layout->header.y() + layout->header.height()) + : layout->content.y(); + return base + st::storiesHeaderMargin.bottom(); + }); +} + void Controller::showFullCaption() { if (_captionText.empty()) { return; @@ -836,6 +857,9 @@ void Controller::show( _waitingForId = {}; _waitingForDelta = 0; _videoStream = (story->call() != nullptr); + if (!_videoStream) { + clearVideoStreamCall(); + } rebuildFromContext(peer, storyId); _contextLifetime.destroy(); @@ -905,6 +929,7 @@ void Controller::show( return; } + clearVideoStreamCall(); _replyArea->show({ .peer = unsupported ? nullptr : peer.get(), .id = story->id(), @@ -918,8 +943,10 @@ void Controller::show( .forwards = story->forwards(), .views = story->views(), .total = story->interactions(), - .type = RecentViewsTypeFor(peer), - .canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(), + .type = RecentViewsTypeFor(peer, _videoStream), + .canViewReactions = (!_videoStream + && CanViewReactionsFor(peer) + && !peer->isMegagroup()), }, _reactions->likedValue()); if (const auto nowLikeButton = _recentViews->likeButton()) { if (wasLikeButton != nowLikeButton) { @@ -1025,8 +1052,10 @@ void Controller::subscribeToSession() { .forwards = update.story->forwards(), .views = update.story->views(), .total = update.story->interactions(), - .type = RecentViewsTypeFor(peer), - .canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(), + .type = RecentViewsTypeFor(peer, _videoStream), + .canViewReactions = (!_videoStream + && CanViewReactionsFor(peer) + && !peer->isMegagroup()), }); updateAreas(update.story); } @@ -1742,10 +1771,14 @@ auto Controller::starsReactionsValue() const } void Controller::setStarsReactionIncrements(rpl::producer<> increments) { - _starsReactions = std::move(increments) | rpl::map([=] { - _mineStarReaction = true; - return _starsReactions.current() + 1; - }); + std::move( + increments + ) | rpl::start_with_next([=] { + if (const auto call = _videoStreamCall.get()) { + const auto show = _delegate->storiesShow(); + Payments::TryAddingPaidReaction(call, 1, std::nullopt, show); + } + }, _videoStreamLifetime); } void Controller::shareRequested() { @@ -1867,9 +1900,25 @@ void Controller::updateVideoStream(not_null videoStream) { _commentsHasUnread = has; }, _lifetime); + _starsReactions = rpl::single(rpl::empty) | rpl::then( + videoStream->messages()->starsValueChanges() + ) | rpl::map([=] { + const auto state = videoStream->messages()->starsLocalState(); + + _mineStarReaction = (state.my > 0); + return state.total; + }); + _replyArea->updateVideoStream(videoStream); } +void Controller::clearVideoStreamCall() { + _videoStreamCall = nullptr; + _mineStarReaction = false; + _starsReactions = 0; + _videoStreamLifetime.destroy(); +} + bool Controller::videoStream() const { return _videoStream; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 4a67abeede..d9fed85ad1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -42,6 +42,10 @@ struct ChosenReaction; enum class AttachSelectorResult; } // namespace HistoryView::Reactions +namespace HistoryView { +class PaidReactionToast; +} // namespace HistoryView + namespace Ui { class RpWidget; class BoxContent; @@ -81,6 +85,7 @@ class StoryAreaView; struct RepostClickHandler; using CommentsState = HistoryView::Controls::ToggleCommentsState; +using PaidReactionToast = HistoryView::PaidReactionToast; enum class HeaderLayout { Normal, @@ -283,6 +288,8 @@ private: [[nodiscard]] int repostSkipTop() const; void updateAreas(Data::Story *story); bool reactionChosen(ReactionsMode mode, ChosenReaction chosen); + [[nodiscard]] rpl::producer paidReactionToastTopValue() const; + void clearVideoStreamCall(); const not_null _delegate; @@ -300,6 +307,8 @@ private: std::unique_ptr _repostView; base::weak_ptr _videoStreamCall; + std::unique_ptr _paidReactionToast; + rpl::lifetime _videoStreamLifetime; Ui::Animations::Simple _contentFadeAnimation; bool _contentFaded = false; diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp index 6645637258..940ed39c5b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -137,8 +137,12 @@ constexpr auto kLoadViewsPages = 2; } // namespace -RecentViewsType RecentViewsTypeFor(not_null peer) { - return peer->isSelf() +RecentViewsType RecentViewsTypeFor( + not_null peer, + bool videoStream) { + return videoStream + ? RecentViewsType::Other + : peer->isSelf() ? RecentViewsType::Self : peer->isBroadcast() ? RecentViewsType::Channel diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h index ea8d69313c..83dd2efde2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h @@ -58,7 +58,9 @@ struct RecentViewsData { const RecentViewsData &) = default; }; -[[nodiscard]] RecentViewsType RecentViewsTypeFor(not_null peer); +[[nodiscard]] RecentViewsType RecentViewsTypeFor( + not_null peer, + bool videoStream); [[nodiscard]] bool CanViewReactionsFor(not_null peer); class RecentViews final { diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 61b479590f..d4c9202064 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -906,7 +906,8 @@ void ReplyArea::show( }); _controls->clear(); const auto hidden = peer - && (peer->isBroadcast() || peer->isSelf() || peer->isServiceUser()); + && (peer->isBroadcast() || peer->isSelf() || peer->isServiceUser()) + && !_data.videoStream; const auto cant = !peer; if (!hidden && !cant) { _controls->show(); diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index cf394237b9..71d2395e3a 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "boxes/peers/prepare_short_info_box.h" #include "boxes/send_credits_box.h" // CreditsEmojiSmall. +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_messages.h" #include "core/ui_integration.h" // TextContext. #include "data/components/credits.h" #include "data/data_channel.h" @@ -43,15 +45,15 @@ constexpr auto kMaxPerReactionFallback = 10'000; constexpr auto kDefaultPerReaction = 50; void TryAddingPaidReaction( - not_null session, + std::shared_ptr show, FullMsgId itemId, base::weak_ptr weakView, int count, std::optional shownPeer, - std::shared_ptr show, Fn finished) { + const auto owner = &show->session().data(); const auto checkItem = [=] { - const auto item = session->data().message(itemId); + const auto item = owner->message(itemId); if (!item) { if (const auto onstack = finished) { onstack(false); @@ -86,7 +88,7 @@ void TryAddingPaidReaction( }; const auto channelId = peerToChannel(itemId.peer); Settings::MaybeRequestBalanceIncrease( - Main::MakeSessionShow(show, session), + show, count, Settings::SmallBalanceReaction{ .channelId = channelId }, done); @@ -108,18 +110,54 @@ void TryAddingPaidReaction( HistoryView::Element *view, int count, std::optional shownPeer, - std::shared_ptr show, + std::shared_ptr show, Fn finished) { TryAddingPaidReaction( - &item->history()->session(), + std::move(show), item->fullId(), view, count, shownPeer, - std::move(show), std::move(finished)); } +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] { + const auto strong = weak.get(); + if (!strong) { + if (const auto onstack = finished) { + onstack(false); + } + } + return strong; + }; + + const auto done = [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Success + || result == Settings::SmallBalanceResult::Already) { + if (const auto call = checkCall()) { + call->messages()->reactionsPaidAdd(count, shownPeer); + call->peer()->owner().notifyCallPaidReactionSent(call); + if (const auto onstack = finished) { + onstack(true); + } + } + } else if (const auto onstack = finished) { + onstack(false); + } + }; + Settings::MaybeRequestBalanceIncrease( + show, + count, + Settings::SmallBalanceVideoStream{ .streamerId = call->peer()->id }, + done); +} + void ShowPaidReactionDetails( not_null controller, not_null item, diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.h b/Telegram/SourceFiles/payments/payments_reaction_process.h index 571d50c643..9dea2b254f 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.h +++ b/Telegram/SourceFiles/payments/payments_reaction_process.h @@ -11,13 +11,17 @@ enum class HistoryReactionSource : char; class HistoryItem; +namespace Calls { +class GroupCall; +} // namespace Calls + namespace HistoryView { class Element; } // namespace HistoryView -namespace Ui { -class Show; -} // namespace Ui +namespace Main { +class SessionShow; +} // namespace Main namespace Window { class SessionController; @@ -32,7 +36,14 @@ void TryAddingPaidReaction( HistoryView::Element *view, int count, std::optional shownPeer, - std::shared_ptr show, + std::shared_ptr show, + Fn finished = nullptr); + +void TryAddingPaidReaction( + not_null call, + int count, + std::optional shownPeer, + std::shared_ptr show, Fn finished = nullptr); void ShowPaidReactionDetails( diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 73c50b384c..ee8e31bb2c 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -2892,6 +2892,9 @@ void SmallBalanceBox( : QString(); }, [&](SmallBalanceReaction value) { return owner->peer(peerFromChannel(value.channelId))->name(); + }, [&](SmallBalanceVideoStream value) { + dark = true; + return owner->peer(value.streamerId)->name(); }, [](SmallBalanceSubscription value) { return value.name; }, [](SmallBalanceDeepLink) { @@ -2940,6 +2943,11 @@ void SmallBalanceBox( lt_channel, rpl::single(Ui::Text::Bold(name)), Ui::Text::RichLangValue) + : v::is(source) + ? tr::lng_credits_small_balance_video_stream( + lt_name, + rpl::single(Ui::Text::Bold(name)), + Ui::Text::RichLangValue) : v::is(source) ? DeepLinkBalanceAbout( v::get(source).purpose) diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index f0f3284ab8..4df951e40c 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -234,6 +234,9 @@ struct SmallBalanceBot { struct SmallBalanceReaction { ChannelId channelId = 0; }; +struct SmallBalanceVideoStream { + PeerId streamerId = 0; +}; struct SmallBalanceSubscription { QString name; }; @@ -254,6 +257,7 @@ struct SmallBalanceForSearch { struct SmallBalanceSource : std::variant< SmallBalanceBot, SmallBalanceReaction, + SmallBalanceVideoStream, SmallBalanceSubscription, SmallBalanceDeepLink, SmallBalanceStarGift,