diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 7ee4e538b..cb934282b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -826,6 +826,8 @@ PRIVATE history/view/history_view_message.cpp history/view/history_view_message.h history/view/history_view_object.h + history/view/history_view_paid_reaction_toast.cpp + history/view/history_view_paid_reaction_toast.h history/view/history_view_pinned_bar.cpp history/view/history_view_pinned_bar.h history/view/history_view_pinned_section.cpp diff --git a/Telegram/Resources/animations/star_reaction/toast.tgs b/Telegram/Resources/animations/star_reaction/toast.tgs new file mode 100644 index 000000000..abfb9602c Binary files /dev/null and b/Telegram/Resources/animations/star_reaction/toast.tgs differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 13cab0bee..cb07fac5b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3426,6 +3426,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_paid_react_send" = "Send {price}"; "lng_paid_react_agree" = "By sending stars, you agree to the {link}."; "lng_paid_react_agree_link" = "Terms of Service"; +"lng_paid_react_toast_title" = "Star Sent!"; +"lng_paid_react_toast_text#one" = "You reacted with **{count} Star**."; +"lng_paid_react_toast_text#other" = "You reacted with **{count} Stars**."; +"lng_paid_react_undo" = "Undo"; "lng_translate_show_original" = "Show Original"; "lng_translate_bar_to" = "Translate to {name}"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 6292da0c4..ac6ff1dbf 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -43,6 +43,7 @@ ../../animations/star_reaction/appear.tgs ../../animations/star_reaction/center.tgs ../../animations/star_reaction/select.tgs + ../../animations/star_reaction/toast.tgs ../../animations/star_reaction/effect1.tgs ../../animations/star_reaction/effect2.tgs ../../animations/star_reaction/effect3.tgs diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp index 314a99a8a..095abbb57 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp @@ -24,6 +24,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "main/main_session.h" +#include + namespace ChatHelpers { namespace { @@ -315,6 +317,12 @@ QSize ComputeStickerSize(not_null document, QSize box) { return HistoryView::NonEmptySize(request.size(dimensions, 8) / ratio); } +[[nodiscard]] uint64 LocalTgsStickerId(QStringView name) { + auto full = u"local_tgs_sticker:"_q; + full.append(name); + return XXH64(full.data(), full.size() * sizeof(QChar), 0); +} + not_null GenerateLocalTgsSticker( not_null session, const QString &name) { @@ -327,7 +335,9 @@ not_null GenerateLocalTgsSticker( SendMediaType::File, FileLoadTo(0, {}, {}, 0), {}, - false); + false, + nullptr, + LocalTgsStickerId(name)); task.process({ .generateGoodThumbnail = false }); const auto result = task.peekResult(); Assert(result != nullptr); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index ff21d3d05..cf783d1ee 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -48,7 +48,7 @@ constexpr auto kRecentReactionsLimit = 40; constexpr auto kMyTagsRequestTimeout = crl::time(1000); constexpr auto kTopRequestDelay = 60 * crl::time(1000); constexpr auto kTopReactionsLimit = 14; -constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000); +constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500; [[nodiscard]] QString ReactionIdToLog(const ReactionId &id) { if (const auto custom = id.custom()) { @@ -1525,6 +1525,15 @@ not_null Reactions::lookupPaid() { return &*_paid; } +not_null Reactions::paidToastAnimation() { + if (!_paidToastAnimation) { + _paidToastAnimation = ChatHelpers::GenerateLocalTgsSticker( + &_owner->session(), + u"star_reaction_toast"_q); + } + return _paidToastAnimation; +} + rpl::producer> Reactions::myTagsValue( SavedSublist *sublist) { refreshMyTags(sublist); @@ -1546,6 +1555,21 @@ void Reactions::schedulePaid(not_null item) { } } +void Reactions::undoScheduledPaid(not_null item) { + _sendPaidItems.remove(item); + item->cancelScheduledPaidReaction(); +} + +crl::time Reactions::sendingScheduledPaidAt( + not_null item) const { + const auto i = _sendPaidItems.find(item); + return (i != end(_sendPaidItems)) ? i->second : crl::time(); +} + +crl::time Reactions::ScheduledPaidDelay() { + return kPaidAccumulatePeriod; +} + void Reactions::repaintCollected() { const auto now = crl::now(); auto closest = crl::time(); diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 5ab61ac50..bc61c7c4c 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -139,11 +139,16 @@ public: void clearTemporary(); [[nodiscard]] Reaction *lookupTemporary(const ReactionId &id); [[nodiscard]] not_null lookupPaid(); + [[nodiscard]] not_null paidToastAnimation(); [[nodiscard]] rpl::producer> myTagsValue( SavedSublist *sublist = nullptr); void schedulePaid(not_null item); + void undoScheduledPaid(not_null item); + [[nodiscard]] crl::time sendingScheduledPaidAt( + not_null item) const; + [[nodiscard]] static crl::time ScheduledPaidDelay(); [[nodiscard]] static bool HasUnread(const MTPMessageReactions &data); static void CheckUnknownForUnread( @@ -293,6 +298,7 @@ private: // Otherwise we could use flat_map>. std::map _temporary; std::optional _paid; + DocumentData *_paidToastAnimation = nullptr; base::Timer _topRefreshTimer; mtpRequestId _topRequestId = 0; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index a673ebca5..ec4d1c6e4 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1884,6 +1884,14 @@ rpl::producer> Session::viewRemoved() const { return _viewRemoved.events(); } +void Session::notifyViewPaidReactionSent(not_null view) { + _viewPaidReactionSent.fire_copy(view); +} + +rpl::producer> Session::viewPaidReactionSent() const { + return _viewPaidReactionSent.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 06b4b267d..5abb66c98 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -306,6 +306,8 @@ public: [[nodiscard]] rpl::producer> historyCleared() const; void notifyHistoryChangeDelayed(not_null history); [[nodiscard]] rpl::producer> historyChanged() const; + void notifyViewPaidReactionSent(not_null view); + [[nodiscard]] rpl::producer> viewPaidReactionSent() const; void sendHistoryChangeNotifications(); void notifyPinnedDialogsOrderUpdated(); @@ -923,6 +925,7 @@ private: rpl::event_stream> _itemDataChanges; rpl::event_stream> _itemRemoved; rpl::event_stream> _viewRemoved; + rpl::event_stream> _viewPaidReactionSent; rpl::event_stream> _historyUnloaded; rpl::event_stream> _historyCleared; base::flat_set> _historiesChanged; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 69287a9cf..a8e74a4f5 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2525,6 +2525,13 @@ void HistoryItem::addPaidReaction(int count) { _history->owner().notifyItemDataChange(this); } +void HistoryItem::cancelScheduledPaidReaction() { + if (_reactions) { + _reactions->cancelScheduledPaid(); + _history->owner().notifyItemDataChange(this); + } +} + int HistoryItem::startPaidReactionSending() { return _reactions ? _reactions->startPaidSending() : 0; } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index d9b48dfd5..be5c7f7a9 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -446,6 +446,7 @@ public: const Data::ReactionId &reaction, HistoryReactionSource source); void addPaidReaction(int count); + void cancelScheduledPaidReaction(); [[nodiscard]] int startPaidReactionSending(); void finishPaidReactionSending(int count, bool success); void updateReactionsUnknown(); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 6356344f9..86012f331 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -104,6 +104,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_top_bar_widget.h" #include "history/view/history_view_contact_status.h" #include "history/view/history_view_context_menu.h" +#include "history/view/history_view_paid_reaction_toast.h" #include "history/view/history_view_pinned_tracker.h" #include "history/view/history_view_pinned_section.h" #include "history/view/history_view_pinned_bar.h" @@ -285,6 +286,13 @@ HistoryWidget::HistoryWidget( }) , _saveDraftTimer([=] { saveDraft(); }) , _saveCloudDraftTimer([=] { saveCloudDraft(); }) +, _paidReactionToast(std::make_unique( + this, + &session().data(), + rpl::single(st::topBarHeight), + [=](not_null view) { + return _list && _list->itemTop(view) >= 0; + })) , _topShadow(this) { setAcceptDrops(true); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index b90ef037d..ef17e3285 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -87,6 +87,7 @@ class TabbedSelector; namespace HistoryView { class StickerToast; +class PaidReactionToast; class TopBarWidget; class ContactStatus; class BusinessBotStatus; @@ -825,6 +826,8 @@ private: std::unique_ptr _stickerToast; std::unique_ptr _chooseForReport; + std::unique_ptr _paidReactionToast; + base::flat_set> _itemRevealPending; base::flat_map< not_null, diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp new file mode 100644 index 000000000..a4125a076 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp @@ -0,0 +1,216 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/history_view_paid_reaction_toast.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_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/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 "lottie/lottie_single_player.h" +//#include "window/window_session_controller.h" +//#include "settings/settings_premium.h" +//#include "apiwrap.h" +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +constexpr auto kPremiumToastDuration = 5 * crl::time(1000); + +} // namespace + +PaidReactionToast::PaidReactionToast( + not_null parent, + not_null owner, + rpl::producer topOffset, + Fn view)> mine) +: _parent(parent) +, _owner(owner) +, _topOffset(std::move(topOffset)) { + _owner->viewPaidReactionSent( + ) | rpl::filter( + std::move(mine) + ) | rpl::start_with_next([=](not_null view) { + maybeShowFor(view->data()); + }, _lifetime); +} + +PaidReactionToast::~PaidReactionToast() { + _hiding.push_back(_weak); + for (const auto &weak : base::take(_hiding)) { + if (const auto strong = weak.get()) { + delete strong->widget(); + } + } +} + +void PaidReactionToast::maybeShowFor(not_null item) { + const auto count = item->reactionsPaidScheduled(); + const auto at = _owner->reactions().sendingScheduledPaidAt(item); + if (!count || !at) { + return; + } + 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); + } +} + +void PaidReactionToast::showFor( + FullMsgId itemId, + int count, + crl::time left, + 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); + return; + } + _stack.erase(i); + } + _stack.push_back(itemId); + + clearHiddenHiding(); + if (old) { + old->hideAnimated(); + _hiding.push_back(_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); + + _weak = Ui::Toast::Show(_parent, Ui::Toast::Config{ + .text = text, + .st = &_st, + .duration = -1, + .multiline = true, + .dark = true, + .slideSide = RectPart::Top, + }); + const auto strong = _weak.get(); + if (!strong) { + return; + } + strong->setInputUsed(true); + const auto widget = strong->widget(); + const auto hideToast = [weak = _weak] { + if (const auto strong = weak.get()) { + strong->hideAnimated(); + } + }; + + const auto button = Ui::CreateChild( + widget.get(), + rpl::single(undo), + st::historyPremiumViewSet); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->show(); + rpl::combine( + widget->sizeValue(), + button->sizeValue() + ) | rpl::start_with_next([=](QSize outer, QSize inner) { + button->moveToRight( + 0, + (outer.height() - inner.height()) / 2, + outer.width()); + }, widget->lifetime()); + const auto preview = Ui::CreateChild(widget.get()); + preview->moveToLeft(skip, skip); + preview->resize(size, size); + 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() { + _hiding.erase( + ranges::remove( + _hiding, + nullptr, + &base::weak_ptr::get), + end(_hiding)); +} + +void PaidReactionToast::setupLottiePreview( + not_null widget, + int size) { + const auto generate = [&](const QString &name) { + const auto session = &_owner->session(); + return ChatHelpers::GenerateLocalTgsSticker(session, name); + }; + const auto document = _owner->reactions().paidToastAnimation(); + + const auto bytes = document->createMediaView()->bytes(); + const auto filepath = document->filepath(); + const auto player = widget->lifetime().make_state( + Lottie::ReadContent(bytes, filepath), + Lottie::FrameRequest{ QSize(size, size) }, + Lottie::Quality::Default); + + widget->paintRequest( + ) | rpl::start_with_next([=] { + if (!player->ready()) { + return; + } + const auto image = player->frame(); + QPainter(widget).drawImage( + QRect(QPoint(), image.size() / image.devicePixelRatio()), + image); + if (player->frameIndex() + 1 != player->framesCount()) { + player->markFrameShown(); + } + }, widget->lifetime()); + + player->updates( + ) | rpl::start_with_next([=] { + widget->update(); + }, widget->lifetime()); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h new file mode 100644 index 000000000..5b42ad806 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h @@ -0,0 +1,68 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "styles/style_widgets.h" + +namespace Data { +class Session; +} // namespace Data + +namespace Ui { +//class Show; +class RpWidget; +} // namespace Ui + +namespace Ui::Toast { +class Instance; +} // namespace Ui::Toast + +namespace HistoryView { + +class Element; + +class PaidReactionToast final { +public: + PaidReactionToast( + not_null parent, + not_null owner, + rpl::producer topOffset, + Fn view)> mine); + ~PaidReactionToast(); + +private: + void 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(); + + const not_null _parent; + const not_null _owner; + const rpl::variable _topOffset; + + style::Toast _st; + base::weak_ptr _weak; + std::vector> _hiding; + + std::vector _stack; + + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index 5e40068b1..0de8a9147 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -62,6 +62,8 @@ void TryAddingPaidReaction( if (const auto item = checkItem()) { item->addPaidReaction(count); if (const auto view = weakView.get()) { + const auto history = view->history(); + history->owner().notifyViewPaidReactionSent(view); view->animateReaction({ .id = Data::ReactionId::Paid(), }); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 1895a8703..dc3751b75 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -472,8 +472,9 @@ FileLoadTask::FileLoadTask( const FileLoadTo &to, const TextWithTags &caption, bool spoiler, - std::shared_ptr album) -: _id(base::RandomValue()) + std::shared_ptr album, + uint64 idOverride) +: _id(idOverride ? idOverride : base::RandomValue()) , _session(session) , _dcId(session->mainDcId()) , _to(to) diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h index 2756ac244..d4e99177f 100644 --- a/Telegram/SourceFiles/storage/localimageloader.h +++ b/Telegram/SourceFiles/storage/localimageloader.h @@ -224,7 +224,8 @@ public: const FileLoadTo &to, const TextWithTags &caption, bool spoiler, - std::shared_ptr album = nullptr); + std::shared_ptr album = nullptr, + uint64 idOverride = 0); FileLoadTask( not_null session, const QByteArray &voice, diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 95229cd46..40df9722c 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 95229cd46bbba42b431a097705494ec39cce5f0c +Subproject commit 40df9722c97385277f128c21a7fcfc13da52b7c7