Start paid reaction toast notification.

This commit is contained in:
John Preston 2024-08-08 10:56:31 +02:00
parent 02610de010
commit ac92e1c99e
19 changed files with 371 additions and 6 deletions

View file

@ -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

Binary file not shown.

View file

@ -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}";

View file

@ -43,6 +43,7 @@
<file alias="star_reaction_appear.tgs">../../animations/star_reaction/appear.tgs</file>
<file alias="star_reaction_center.tgs">../../animations/star_reaction/center.tgs</file>
<file alias="star_reaction_select.tgs">../../animations/star_reaction/select.tgs</file>
<file alias="star_reaction_toast.tgs">../../animations/star_reaction/toast.tgs</file>
<file alias="star_reaction_effect1.tgs">../../animations/star_reaction/effect1.tgs</file>
<file alias="star_reaction_effect2.tgs">../../animations/star_reaction/effect2.tgs</file>
<file alias="star_reaction_effect3.tgs">../../animations/star_reaction/effect3.tgs</file>

View file

@ -24,6 +24,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "main/main_session.h"
#include <xxhash.h>
namespace ChatHelpers {
namespace {
@ -315,6 +317,12 @@ QSize ComputeStickerSize(not_null<DocumentData*> 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<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name) {
@ -327,7 +335,9 @@ not_null<DocumentData*> GenerateLocalTgsSticker(
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
false);
false,
nullptr,
LocalTgsStickerId(name));
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);

View file

@ -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<Reaction*> Reactions::lookupPaid() {
return &*_paid;
}
not_null<DocumentData*> Reactions::paidToastAnimation() {
if (!_paidToastAnimation) {
_paidToastAnimation = ChatHelpers::GenerateLocalTgsSticker(
&_owner->session(),
u"star_reaction_toast"_q);
}
return _paidToastAnimation;
}
rpl::producer<std::vector<Reaction>> Reactions::myTagsValue(
SavedSublist *sublist) {
refreshMyTags(sublist);
@ -1546,6 +1555,21 @@ void Reactions::schedulePaid(not_null<HistoryItem*> item) {
}
}
void Reactions::undoScheduledPaid(not_null<HistoryItem*> item) {
_sendPaidItems.remove(item);
item->cancelScheduledPaidReaction();
}
crl::time Reactions::sendingScheduledPaidAt(
not_null<HistoryItem*> 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();

View file

@ -139,11 +139,16 @@ public:
void clearTemporary();
[[nodiscard]] Reaction *lookupTemporary(const ReactionId &id);
[[nodiscard]] not_null<Reaction*> lookupPaid();
[[nodiscard]] not_null<DocumentData*> paidToastAnimation();
[[nodiscard]] rpl::producer<std::vector<Reaction>> myTagsValue(
SavedSublist *sublist = nullptr);
void schedulePaid(not_null<HistoryItem*> item);
void undoScheduledPaid(not_null<HistoryItem*> item);
[[nodiscard]] crl::time sendingScheduledPaidAt(
not_null<HistoryItem*> 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<DocumentId, unique_ptr<Reaction>>.
std::map<DocumentId, Reaction> _temporary;
std::optional<Reaction> _paid;
DocumentData *_paidToastAnimation = nullptr;
base::Timer _topRefreshTimer;
mtpRequestId _topRequestId = 0;

View file

@ -1884,6 +1884,14 @@ rpl::producer<not_null<const ViewElement*>> Session::viewRemoved() const {
return _viewRemoved.events();
}
void Session::notifyViewPaidReactionSent(not_null<const ViewElement*> view) {
_viewPaidReactionSent.fire_copy(view);
}
rpl::producer<not_null<const ViewElement*>> Session::viewPaidReactionSent() const {
return _viewPaidReactionSent.events();
}
void Session::notifyHistoryUnloaded(not_null<const History*> history) {
_historyUnloaded.fire_copy(history);
}

View file

@ -306,6 +306,8 @@ public:
[[nodiscard]] rpl::producer<not_null<const History*>> historyCleared() const;
void notifyHistoryChangeDelayed(not_null<History*> history);
[[nodiscard]] rpl::producer<not_null<History*>> historyChanged() const;
void notifyViewPaidReactionSent(not_null<const ViewElement*> view);
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewPaidReactionSent() const;
void sendHistoryChangeNotifications();
void notifyPinnedDialogsOrderUpdated();
@ -923,6 +925,7 @@ private:
rpl::event_stream<not_null<HistoryItem*>> _itemDataChanges;
rpl::event_stream<not_null<const HistoryItem*>> _itemRemoved;
rpl::event_stream<not_null<const ViewElement*>> _viewRemoved;
rpl::event_stream<not_null<const ViewElement*>> _viewPaidReactionSent;
rpl::event_stream<not_null<const History*>> _historyUnloaded;
rpl::event_stream<not_null<const History*>> _historyCleared;
base::flat_set<not_null<History*>> _historiesChanged;

View file

@ -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;
}

View file

@ -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();

View file

@ -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<HistoryView::PaidReactionToast>(
this,
&session().data(),
rpl::single(st::topBarHeight),
[=](not_null<const HistoryView::Element*> view) {
return _list && _list->itemTop(view) >= 0;
}))
, _topShadow(this) {
setAcceptDrops(true);

View file

@ -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<HistoryView::StickerToast> _stickerToast;
std::unique_ptr<ChooseMessagesForReport> _chooseForReport;
std::unique_ptr<HistoryView::PaidReactionToast> _paidReactionToast;
base::flat_set<not_null<HistoryItem*>> _itemRevealPending;
base::flat_map<
not_null<HistoryItem*>,

View file

@ -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<Ui::RpWidget*> parent,
not_null<Data::Session*> owner,
rpl::producer<int> topOffset,
Fn<bool(not_null<const Element*> view)> mine)
: _parent(parent)
, _owner(owner)
, _topOffset(std::move(topOffset)) {
_owner->viewPaidReactionSent(
) | rpl::filter(
std::move(mine)
) | rpl::start_with_next([=](not_null<const Element*> 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<HistoryItem*> 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<Ui::RoundButton>(
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<Ui::RpWidget>(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<Ui::Toast::Instance*> toast,
int count,
crl::time left,
crl::time total) {
}
void PaidReactionToast::clearHiddenHiding() {
_hiding.erase(
ranges::remove(
_hiding,
nullptr,
&base::weak_ptr<Ui::Toast::Instance>::get),
end(_hiding));
}
void PaidReactionToast::setupLottiePreview(
not_null<Ui::RpWidget*> 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::SinglePlayer>(
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

View file

@ -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<Ui::RpWidget*> parent,
not_null<Data::Session*> owner,
rpl::producer<int> topOffset,
Fn<bool(not_null<const Element*> view)> mine);
~PaidReactionToast();
private:
void maybeShowFor(not_null<HistoryItem*> item);
void showFor(
FullMsgId itemId,
int count,
crl::time left,
crl::time total);
void update(
not_null<Ui::Toast::Instance*> toast,
int count,
crl::time left,
crl::time total);
void setupLottiePreview(not_null<Ui::RpWidget*> widget, int size);
void clearHiddenHiding();
const not_null<Ui::RpWidget*> _parent;
const not_null<Data::Session*> _owner;
const rpl::variable<int> _topOffset;
style::Toast _st;
base::weak_ptr<Ui::Toast::Instance> _weak;
std::vector<base::weak_ptr<Ui::Toast::Instance>> _hiding;
std::vector<FullMsgId> _stack;
rpl::lifetime _lifetime;
};
} // namespace HistoryView

View file

@ -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(),
});

View file

@ -472,8 +472,9 @@ FileLoadTask::FileLoadTask(
const FileLoadTo &to,
const TextWithTags &caption,
bool spoiler,
std::shared_ptr<SendingAlbum> album)
: _id(base::RandomValue<uint64>())
std::shared_ptr<SendingAlbum> album,
uint64 idOverride)
: _id(idOverride ? idOverride : base::RandomValue<uint64>())
, _session(session)
, _dcId(session->mainDcId())
, _to(to)

View file

@ -224,7 +224,8 @@ public:
const FileLoadTo &to,
const TextWithTags &caption,
bool spoiler,
std::shared_ptr<SendingAlbum> album = nullptr);
std::shared_ptr<SendingAlbum> album = nullptr,
uint64 idOverride = 0);
FileLoadTask(
not_null<Main::Session*> session,
const QByteArray &voice,

@ -1 +1 @@
Subproject commit 95229cd46bbba42b431a097705494ec39cce5f0c
Subproject commit 40df9722c97385277f128c21a7fcfc13da52b7c7