Show premium emoji tooltip on paste.

This commit is contained in:
John Preston 2022-07-15 17:52:36 +03:00
parent 4ca6af33d4
commit 5ce8ed80bf
19 changed files with 225 additions and 49 deletions

View File

@ -226,6 +226,7 @@ void ShareBox::prepareCommentField() {
_show,
field,
nullptr,
nullptr,
_descriptor.stLabel);
}
field->setSubmitSettings(Core::App().settings().sendSubmitWay());

View File

@ -474,7 +474,7 @@ void EmojiListWidget::repaintLater(
DocumentId documentId,
uint64 setId,
Ui::CustomEmoji::RepaintRequest request) {
if (_instances.empty()) {
if (_instances.empty() || !request.when) {
return;
}
auto &repaint = _repaints[request.duration];

View File

@ -54,23 +54,29 @@ constexpr auto kParseLinksTimeout = crl::time(1000);
// ignore tags for different users.
class FieldTagMimeProcessor final {
public:
explicit FieldTagMimeProcessor(not_null<Main::Session*> _session);
FieldTagMimeProcessor(
not_null<Main::Session*> _session,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted);
QString operator()(QStringView mimeTag);
private:
const not_null<Main::Session*> _session;
const Fn<void(not_null<DocumentData*>)> _unavailableEmojiPasted;
};
FieldTagMimeProcessor::FieldTagMimeProcessor(
not_null<Main::Session*> session)
: _session(session) {
not_null<Main::Session*> session,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted)
: _session(session)
, _unavailableEmojiPasted(unavailableEmojiPasted) {
}
QString FieldTagMimeProcessor::operator()(QStringView mimeTag) {
const auto id = _session->userId().bare;
auto all = TextUtilities::SplitTags(mimeTag);
auto premiumSkipped = (DocumentData*)nullptr;
for (auto i = all.begin(); i != all.end();) {
const auto tag = *i;
if (TextUtilities::IsMentionLink(tag)
@ -86,7 +92,8 @@ QString FieldTagMimeProcessor::operator()(QStringView mimeTag) {
}
if (!_session->premium()) {
const auto document = _session->data().document(emoji.id);
if (document->isPremiumSticker()) {
if (document->isPremiumEmoji()) {
premiumSkipped = document;
i = all.erase(i);
continue;
}
@ -94,6 +101,11 @@ QString FieldTagMimeProcessor::operator()(QStringView mimeTag) {
}
++i;
}
if (premiumSkipped
&& _session->premiumPossible()
&& _unavailableEmojiPasted) {
_unavailableEmojiPasted(premiumSkipped);
}
return TextUtilities::JoinTag(all);
}
@ -301,8 +313,10 @@ void InitMessageFieldHandlers(
std::shared_ptr<Ui::Show> show,
not_null<Ui::InputField*> field,
Fn<bool()> customEmojiPaused,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted,
const style::InputField *fieldStyle) {
field->setTagMimeProcessor(FieldTagMimeProcessor(session));
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, unavailableEmojiPasted));
field->setCustomEmojiFactory([=](QStringView data, Fn<void()> update) {
return session->data().customEmojiManager().create(
data,
@ -322,12 +336,14 @@ void InitMessageFieldHandlers(
void InitMessageFieldHandlers(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field,
Window::GifPauseReason pauseReasonLevel) {
Window::GifPauseReason pauseReasonLevel,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted) {
InitMessageFieldHandlers(
&controller->session(),
std::make_shared<Window::Show>(controller),
field,
[=] { return controller->isGifPausedAtLeastFor(pauseReasonLevel); });
[=] { return controller->isGifPausedAtLeastFor(pauseReasonLevel); },
unavailableEmojiPasted);
}
void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
@ -341,8 +357,13 @@ void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
void InitMessageField(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field) {
InitMessageFieldHandlers(controller, field, Window::GifPauseReason::Any);
not_null<Ui::InputField*> field,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted) {
InitMessageFieldHandlers(
controller,
field,
Window::GifPauseReason::Any,
unavailableEmojiPasted);
InitMessageFieldGeometry(field);
field->customTab(true);
}

View File

@ -49,14 +49,17 @@ void InitMessageFieldHandlers(
std::shared_ptr<Ui::Show> show,
not_null<Ui::InputField*> field,
Fn<bool()> customEmojiPaused,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted = nullptr,
const style::InputField *fieldStyle = nullptr);
void InitMessageFieldHandlers(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field,
Window::GifPauseReason pauseReasonLevel);
Window::GifPauseReason pauseReasonLevel,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted = nullptr);
void InitMessageField(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field);
not_null<Ui::InputField*> field,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted);
void InitSpellchecker(
std::shared_ptr<Ui::Show> show,

View File

@ -394,7 +394,10 @@ HistoryWidget::HistoryWidget(
return _history ? _history->peer.get() : nullptr;
});
InitMessageField(controller, _field);
InitMessageField(controller, _field, [=](
not_null<DocumentData*> document) {
showPremiumToast(document);
});
_keyboard->sendCommandRequests(
) | rpl::start_with_next([=](Bot::SendCommandRequest r) {
@ -6732,17 +6735,21 @@ void HistoryWidget::showPremiumStickerTooltip(
not_null<const HistoryView::Element*> view) {
if (const auto media = view->data()->media()) {
if (const auto document = media->document()) {
if (!_stickerToast) {
_stickerToast = std::make_unique<HistoryView::StickerToast>(
controller(),
_scroll.data(),
[=] { _stickerToast = nullptr; });
}
_stickerToast->showFor(document);
showPremiumToast(document);
}
}
}
void HistoryWidget::showPremiumToast(not_null<DocumentData*> document) {
if (!_stickerToast) {
_stickerToast = std::make_unique<HistoryView::StickerToast>(
controller(),
_scroll.data(),
[=] { _stickerToast = nullptr; });
}
_stickerToast->showFor(document);
}
void HistoryWidget::setFieldText(
const TextWithTags &textWithTags,
TextUpdateEvents events,

View File

@ -265,6 +265,7 @@ public:
Fn<void()> hiddenCallback);
void showPremiumStickerTooltip(
not_null<const HistoryView::Element*> view);
void showPremiumToast(not_null<DocumentData*> document);
// Tabbed selector management.
bool pushTabbedSelectorToThirdSection(

View File

@ -811,6 +811,7 @@ MessageToEdit FieldHeader::queryToEdit() {
ComposeControls::ComposeControls(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> window,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted,
Mode mode,
SendMenu::Type sendMenuType)
: _parent(parent)
@ -847,6 +848,7 @@ ComposeControls::ComposeControls(
_send,
st::historySendSize.height()))
, _sendMenuType(sendMenuType)
, _unavailableEmojiPasted(unavailableEmojiPasted)
, _saveDraftTimer([=] { saveDraft(); }) {
init();
}
@ -1420,7 +1422,7 @@ void ComposeControls::initField() {
Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); });
//Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); });
Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); });
InitMessageField(_window, _field);
InitMessageField(_window, _field, _unavailableEmojiPasted);
initAutocomplete();
const auto suggestions = Ui::Emoji::SuggestionsController::Init(
_parent,
@ -1963,7 +1965,7 @@ void ComposeControls::initVoiceRecordBar() {
_voiceRecordBar->setStartRecordingFilter([=] {
const auto error = [&]() -> std::optional<QString> {
const auto peer = _history ? _history->peer : nullptr;
const auto peer = _history ? _history->peer.get() : nullptr;
if (!peer) {
const auto type = ChatRestriction::SendMedia;
if (const auto error = Data::RestrictionError(peer, type)) {

View File

@ -97,6 +97,7 @@ public:
ComposeControls(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> window,
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted,
Mode mode,
SendMenu::Type sendMenuType);
~ComposeControls();
@ -309,6 +310,7 @@ private:
const std::unique_ptr<Controls::VoiceRecordBar> _voiceRecordBar;
const SendMenu::Type _sendMenuType;
const Fn<void(not_null<DocumentData*>)> _unavailableEmojiPasted;
rpl::event_stream<Api::SendOptions> _sendCustomRequests;
rpl::event_stream<> _cancelRequests;

View File

@ -1494,6 +1494,12 @@ void ListWidget::elementStartPremium(
not_null<const Element*> view,
Element *replacing) {
_emojiInteractions->playPremiumEffect(view, replacing);
const auto already = !_emojiInteractions->playPremiumEffect(
view,
replacing);
if (already) {
showPremiumStickerTooltip(view);
}
}
void ListWidget::elementCancelPremium(not_null<const Element*> view) {
@ -1599,6 +1605,15 @@ void ListWidget::startMessageSendingAnimation(
});
}
void ListWidget::showPremiumStickerTooltip(
not_null<const HistoryView::Element*> view) {
if (const auto media = view->data()->media()) {
if (const auto document = media->document()) {
_delegate->listShowPremiumToast(document);
}
}
}
void ListWidget::revealItemsCallback() {
auto revealHeight = 0;
for (auto i = begin(_itemRevealAnimations)

View File

@ -119,6 +119,7 @@ public:
virtual CopyRestrictionType listSelectRestrictionType() = 0;
virtual auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> = 0;
virtual void listShowPremiumToast(not_null<DocumentData*> document) = 0;
};
struct SelectionData {
@ -516,6 +517,8 @@ private:
void revealItemsCallback();
void startMessageSendingAnimation(not_null<HistoryItem*> item);
void showPremiumStickerTooltip(
not_null<const HistoryView::Element*> view);
// This function finds all history items that are displayed and calls template method
// for each found message (in given direction) in the passed history with passed top offset.

View File

@ -688,6 +688,9 @@ auto PinnedWidget::listAllowedReactionsValue()
return Data::PeerAllowedReactionsValue(_history->peer);
}
void PinnedWidget::listShowPremiumToast(not_null<DocumentData*> document) {
}
void PinnedWidget::confirmDeleteSelected() {
ConfirmDeleteSelectedItems(_inner);
}

View File

@ -107,6 +107,7 @@ public:
CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> override;
void listShowPremiumToast(not_null<DocumentData*> document) override;
protected:
void resizeEvent(QResizeEvent *e) override;

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_list_widget.h"
#include "history/view/history_view_schedule_box.h"
#include "history/view/history_view_pinned_bar.h"
#include "history/view/history_view_sticker_toast.h"
#include "history/history.h"
#include "history/history_drag_area.h"
#include "history/history_item_components.h"
@ -165,6 +166,7 @@ RepliesWidget::RepliesWidget(
, _composeControls(std::make_unique<ComposeControls>(
this,
controller,
[=](not_null<DocumentData*> emoji) { listShowPremiumToast(emoji); },
ComposeControls::Mode::Normal,
SendMenu::Type::SilentOnly))
, _scroll(std::make_unique<Ui::ScrollArea>(
@ -2043,6 +2045,16 @@ auto RepliesWidget::listAllowedReactionsValue()
return Data::PeerAllowedReactionsValue(_history->peer);
}
void RepliesWidget::listShowPremiumToast(not_null<DocumentData*> document) {
if (!_stickerToast) {
_stickerToast = std::make_unique<HistoryView::StickerToast>(
controller(),
_scroll.get(),
[=] { _stickerToast = nullptr; });
}
_stickerToast->showFor(document);
}
void RepliesWidget::confirmDeleteSelected() {
ConfirmDeleteSelectedItems(_inner);
}

View File

@ -62,6 +62,7 @@ class TopBarWidget;
class RepliesMemento;
class ComposeControls;
class SendActionPainter;
class StickerToast;
class RepliesWidget final
: public Window::SectionWidget
@ -143,6 +144,7 @@ public:
CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> override;
void listShowPremiumToast(not_null<DocumentData*> document) override;
protected:
void resizeEvent(QResizeEvent *e) override;
@ -193,6 +195,8 @@ private:
void clearSelected();
void setPinnedVisibility(bool shown);
void showPremiumToast(not_null<DocumentData*> document);
[[nodiscard]] Api::SendAction prepareSendAction(
Api::SendOptions options) const;
void send();
@ -284,6 +288,7 @@ private:
rpl::variable<bool> _rootVisible = false;
std::unique_ptr<Ui::ScrollArea> _scroll;
std::unique_ptr<HistoryView::StickerToast> _stickerToast;
std::vector<MsgId> _replyReturns;
HistoryItem *_replyReturn = nullptr;

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_top_bar_widget.h"
#include "history/view/history_view_list_widget.h"
#include "history/view/history_view_schedule_box.h"
#include "history/view/history_view_sticker_toast.h"
#include "history/history.h"
#include "history/history_drag_area.h"
#include "history/history_item.h"
@ -107,6 +108,7 @@ ScheduledWidget::ScheduledWidget(
, _composeControls(std::make_unique<ComposeControls>(
this,
controller,
[=](not_null<DocumentData*> emoji) { listShowPremiumToast(emoji); },
ComposeControls::Mode::Scheduled,
SendMenu::Type::Disabled))
, _scrollDown(
@ -1360,6 +1362,17 @@ auto ScheduledWidget::listAllowedReactionsValue()
return rpl::single(std::optional<base::flat_set<QString>>(empty));
}
void ScheduledWidget::listShowPremiumToast(
not_null<DocumentData*> document) {
if (!_stickerToast) {
_stickerToast = std::make_unique<HistoryView::StickerToast>(
controller(),
_scroll.data(),
[=] { _stickerToast = nullptr; });
}
_stickerToast->showFor(document);
}
void ScheduledWidget::confirmSendNowSelected() {
ConfirmSendNowSelectedItems(_inner);
}

View File

@ -48,6 +48,7 @@ class Element;
class TopBarWidget;
class ScheduledMemento;
class ComposeControls;
class StickerToast;
class ScheduledWidget final
: public Window::SectionWidget
@ -128,6 +129,7 @@ public:
CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> override;
void listShowPremiumToast(not_null<DocumentData*> document) override;
protected:
void resizeEvent(QResizeEvent *e) override;
@ -236,6 +238,8 @@ private:
std::unique_ptr<ComposeControls> _composeControls;
bool _skipScrollEvent = false;
std::unique_ptr<HistoryView::StickerToast> _stickerToast;
std::vector<MsgId> _replyReturns;
HistoryItem *_replyReturn = nullptr;

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/toast/toast.h"
#include "ui/toast/toast_widget.h"
#include "ui/widgets/buttons.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
@ -44,9 +45,7 @@ StickerToast::~StickerToast() {
void StickerToast::showFor(not_null<DocumentData*> document) {
const auto sticker = document->sticker();
if (!sticker
|| sticker->type != StickerType::Tgs
|| !document->session().premiumPossible()) {
if (!sticker || !document->session().premiumPossible()) {
return;
} else if (const auto strong = _weak.get()) {
if (_for == document) {
@ -121,10 +120,14 @@ void StickerToast::cancelRequest() {
void StickerToast::showWithTitle(const QString &title) {
Expects(_for != nullptr);
const auto setType = _for->sticker()->setType;
const auto isEmoji = (setType == Data::StickersType::Emoji);
const auto text = Ui::Text::Bold(
title
).append('\n').append(
tr::lng_sticker_premium_text(tr::now)
(isEmoji
? tr::lng_animated_emoji_text(tr::now, Ui::Text::RichLangValue)
: tr::lng_sticker_premium_text(tr::now, Ui::Text::RichLangValue))
);
_st = st::historyPremiumToast;
const auto skip = _st.padding.top();
@ -167,28 +170,11 @@ void StickerToast::showWithTitle(const QString &title) {
preview->resize(size, size);
preview->show();
const auto bytes = _for->createMediaView()->bytes();
const auto filepath = _for->filepath();
const auto player = preview->lifetime().make_state<Lottie::SinglePlayer>(
Lottie::ReadContent(bytes, filepath),
Lottie::FrameRequest{ QSize(size, size) },
Lottie::Quality::Default);
preview->paintRequest(
) | rpl::start_with_next([=] {
if (!player->ready()) {
return;
}
const auto image = player->frame();
QPainter(preview).drawImage(
QRect(QPoint(), image.size() / image.devicePixelRatio()),
image);
player->markFrameShown();
}, preview->lifetime());
player->updates(
) | rpl::start_with_next([=] {
preview->update();
}, preview->lifetime());
if (isEmoji) {
setupEmojiPreview(preview, size);
} else {
setupLottiePreview(preview, size);
}
button->setClickedCallback([=, weak = _weak] {
_controller->show(
Box<StickerSetBox>(_controller, _for->sticker()->set),
@ -199,4 +185,95 @@ void StickerToast::showWithTitle(const QString &title) {
});
}
void StickerToast::setupEmojiPreview(
not_null<Ui::RpWidget*> widget,
int size) {
Expects(_for != nullptr);
struct Instance {
Instance(
std::unique_ptr<Ui::CustomEmoji::Loader> loader,
Fn<void(
not_null<Ui::CustomEmoji::Instance*>,
Ui::CustomEmoji::RepaintRequest)> repaintLater,
Fn<void()> repaint)
: emoji(
Ui::CustomEmoji::Loading(
std::move(loader),
Ui::CustomEmoji::Preview()),
std::move(repaintLater))
, object(&emoji, repaint)
, timer(repaint) {
}
Ui::CustomEmoji::Instance emoji;
Ui::CustomEmoji::Object object;
base::Timer timer;
};
const auto repaintDelayed = [=](
not_null<Ui::CustomEmoji::Instance*> instance,
Ui::CustomEmoji::RepaintRequest request) {
if (!request.when) {
return;
}
const auto now = crl::now();
if (now > request.when) {
reinterpret_cast<Instance*>(instance.get())->timer.callOnce(
now - request.when);
} else {
widget->update();
}
};
const auto instance = widget->lifetime().make_state<Instance>(
_for->owner().customEmojiManager().createLoader(
_for,
Data::CustomEmojiManager::SizeTag::Large),
std::move(repaintDelayed),
[=] { widget->update(); });
widget->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(widget);
const auto paused = false;
const auto size = Ui::Emoji::GetSizeLarge()
/ style::DevicePixelRatio();
instance->object.paint(
p,
(widget->width() - size) / 2,
(widget->height() - size) / 2,
crl::now(),
st::toastBg->c,
paused);
}, widget->lifetime());
}
void StickerToast::setupLottiePreview(not_null<Ui::RpWidget*> widget, int size) {
Expects(_for != nullptr);
const auto bytes = _for->createMediaView()->bytes();
const auto filepath = _for->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);
player->markFrameShown();
}, widget->lifetime());
player->updates(
) | rpl::start_with_next([=] {
widget->update();
}, widget->lifetime());
}
} // namespace HistoryView

View File

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
class Show;
class RpWidget;
} // namespace Ui
namespace Ui::Toast {
@ -39,6 +40,9 @@ private:
void showWithTitle(const QString &title);
[[nodiscard]] QString lookupTitle() const;
void setupEmojiPreview(not_null<Ui::RpWidget*> widget, int size);
void setupLottiePreview(not_null<Ui::RpWidget*> widget, int size);
const not_null<Window::SessionController*> _controller;
const not_null<QWidget*> _parent;
style::Toast _st;

View File

@ -60,7 +60,9 @@ void Document::writeToStream(QDataStream &stream, DocumentData *document) {
}
stream << qint32(document->getDuration());
if (document->type == StickerDocument) {
stream << qint32(document->isPremiumSticker() ? 1 : 0);
const auto premium = document->isPremiumSticker()
|| document->isPremiumEmoji();
stream << qint32(premium ? 1 : 0);
}
writeImageLocation(stream, document->thumbnailLocation());
stream << qint32(document->thumbnailByteSize());