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