mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Support optimized lottie emoji.
This commit is contained in:
parent
9d280da80b
commit
6db3a0ec98
10 changed files with 831 additions and 213 deletions
|
@ -27,6 +27,10 @@ constexpr auto kDontCacheLottieAfterArea = 512 * 512;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
uint8 LottieCacheKeyShift(uint8 replacementsTag, StickerLottieSize sizeTag) {
|
||||||
|
return ((replacementsTag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F);
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Method>
|
template <typename Method>
|
||||||
auto LottieCachedFromContent(
|
auto LottieCachedFromContent(
|
||||||
Method &&method,
|
Method &&method,
|
||||||
|
@ -115,8 +119,9 @@ std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
|
||||||
replacements,
|
replacements,
|
||||||
std::move(renderer));
|
std::move(renderer));
|
||||||
};
|
};
|
||||||
const auto tag = replacements ? replacements->tag : uint8(0);
|
const auto keyShift = LottieCacheKeyShift(
|
||||||
const auto keyShift = ((tag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F);
|
replacements ? replacements->tag : uint8(0),
|
||||||
|
sizeTag);
|
||||||
return LottieFromDocument(method, media, uint8(keyShift), box);
|
return LottieFromDocument(method, media, uint8(keyShift), box);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ using StickersSetFlags = base::flags<StickersSetFlag>;
|
||||||
|
|
||||||
namespace ChatHelpers {
|
namespace ChatHelpers {
|
||||||
|
|
||||||
enum class StickerLottieSize : uchar {
|
enum class StickerLottieSize : uint8 {
|
||||||
MessageHistory,
|
MessageHistory,
|
||||||
StickerSet,
|
StickerSet,
|
||||||
StickersPanel,
|
StickersPanel,
|
||||||
|
@ -66,6 +66,9 @@ enum class StickerLottieSize : uchar {
|
||||||
EmojiInteractionReserved7,
|
EmojiInteractionReserved7,
|
||||||
PremiumReactionPreview,
|
PremiumReactionPreview,
|
||||||
};
|
};
|
||||||
|
[[nodiscard]] uint8 LottieCacheKeyShift(
|
||||||
|
uint8 replacementsTag,
|
||||||
|
StickerLottieSize sizeTag);
|
||||||
|
|
||||||
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
|
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
|
||||||
not_null<Data::DocumentMedia*> media,
|
not_null<Data::DocumentMedia*> media,
|
||||||
|
|
|
@ -15,9 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_file_origin.h"
|
#include "data/data_file_origin.h"
|
||||||
#include "data/stickers/data_stickers_set.h"
|
#include "data/stickers/data_stickers_set.h"
|
||||||
#include "lottie/lottie_common.h"
|
#include "lottie/lottie_common.h"
|
||||||
#include "lottie/lottie_single_player.h"
|
#include "lottie/lottie_emoji.h"
|
||||||
#include "chat_helpers/stickers_lottie.h"
|
#include "chat_helpers/stickers_lottie.h"
|
||||||
#include "ui/text/text_block.h"
|
#include "ui/text/text_block.h"
|
||||||
|
#include "ui/ui_utility.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
|
@ -29,6 +30,25 @@ struct CustomEmojiId {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using SizeTag = CustomEmojiManager::SizeTag;
|
||||||
|
|
||||||
|
[[nodiscard]] ChatHelpers::StickerLottieSize LottieSizeFromTag(SizeTag tag) {
|
||||||
|
using LottieSize = ChatHelpers::StickerLottieSize;
|
||||||
|
switch (tag) {
|
||||||
|
case SizeTag::Normal: return LottieSize::MessageHistory;
|
||||||
|
case SizeTag::Large: return LottieSize::EmojiInteraction;
|
||||||
|
}
|
||||||
|
Unexpected("SizeTag value in CustomEmojiManager-LottieSizeFromTag.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] int SizeFromTag(SizeTag tag) {
|
||||||
|
switch (tag) {
|
||||||
|
case SizeTag::Normal: return Ui::Emoji::GetSizeNormal();
|
||||||
|
case SizeTag::Large: return Ui::Emoji::GetSizeLarge();
|
||||||
|
}
|
||||||
|
Unexpected("SizeTag value in CustomEmojiManager-SizeFromTag.");
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] QString SerializeCustomEmojiId(const CustomEmojiId &id) {
|
[[nodiscard]] QString SerializeCustomEmojiId(const CustomEmojiId &id) {
|
||||||
return QString::number(id.id)
|
return QString::number(id.id)
|
||||||
+ '@'
|
+ '@'
|
||||||
|
@ -37,6 +57,15 @@ namespace {
|
||||||
+ QString::number(id.set.accessHash);
|
+ QString::number(id.set.accessHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QString SerializeCustomEmojiId(
|
||||||
|
not_null<DocumentData*> document) {
|
||||||
|
const auto sticker = document->sticker();
|
||||||
|
return SerializeCustomEmojiId({
|
||||||
|
sticker ? sticker->set : StickerSetIdentifier(),
|
||||||
|
document->id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] CustomEmojiId ParseCustomEmojiData(QStringView data) {
|
[[nodiscard]] CustomEmojiId ParseCustomEmojiData(QStringView data) {
|
||||||
const auto parts = data.split('@');
|
const auto parts = data.split('@');
|
||||||
if (parts.size() != 2) {
|
if (parts.size() != 2) {
|
||||||
|
@ -56,120 +85,83 @@ namespace {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomEmojiWithData {
|
|
||||||
public:
|
|
||||||
explicit CustomEmojiWithData(const QString &data);
|
|
||||||
|
|
||||||
QString entityData();
|
|
||||||
|
|
||||||
private:
|
|
||||||
const QString _data;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
CustomEmojiWithData::CustomEmojiWithData(const QString &data) : _data(data) {
|
|
||||||
}
|
|
||||||
|
|
||||||
QString CustomEmojiWithData::entityData() {
|
|
||||||
return _data;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DocumentCustomEmoji final : public CustomEmojiWithData {
|
|
||||||
public:
|
|
||||||
DocumentCustomEmoji(
|
|
||||||
const QString &data,
|
|
||||||
not_null<DocumentData*> document,
|
|
||||||
Fn<void()> update);
|
|
||||||
|
|
||||||
void paint(QPainter &p, int x, int y, const QColor &preview);
|
|
||||||
|
|
||||||
private:
|
|
||||||
not_null<DocumentData*> _document;
|
|
||||||
std::shared_ptr<Data::DocumentMedia> _media;
|
|
||||||
std::unique_ptr<Lottie::SinglePlayer> _lottie;
|
|
||||||
Fn<void()> _update;
|
|
||||||
rpl::lifetime _lifetime;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
DocumentCustomEmoji::DocumentCustomEmoji(
|
|
||||||
const QString &data,
|
|
||||||
not_null<DocumentData*> document,
|
|
||||||
Fn<void()> update)
|
|
||||||
: CustomEmojiWithData(data)
|
|
||||||
, _document(document)
|
|
||||||
, _update(update) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void DocumentCustomEmoji::paint(QPainter &p, int x, int y, const QColor &preview) {
|
|
||||||
if (!_media) {
|
|
||||||
_media = _document->createMediaView();
|
|
||||||
_media->automaticLoad(_document->stickerSetOrigin(), nullptr);
|
|
||||||
}
|
|
||||||
if (_media->loaded() && !_lottie) {
|
|
||||||
const auto size = Ui::Emoji::GetSizeNormal();
|
|
||||||
_lottie = ChatHelpers::LottiePlayerFromDocument(
|
|
||||||
_media.get(),
|
|
||||||
nullptr,
|
|
||||||
ChatHelpers::StickerLottieSize::MessageHistory,
|
|
||||||
QSize(size, size),
|
|
||||||
Lottie::Quality::High);
|
|
||||||
_lottie->updates() | rpl::start_with_next(_update, _lifetime);
|
|
||||||
}
|
|
||||||
if (_lottie && _lottie->ready()) {
|
|
||||||
const auto frame = _lottie->frame();
|
|
||||||
p.drawImage(
|
|
||||||
QRect(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
frame.width() / frame.devicePixelRatio(),
|
|
||||||
frame.height() / frame.devicePixelRatio()),
|
|
||||||
frame);
|
|
||||||
_lottie->markFrameShown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class CustomEmojiLoader final
|
class CustomEmojiLoader final
|
||||||
: public Ui::CustomEmoji::Loader
|
: public Ui::CustomEmoji::Loader
|
||||||
, public base::has_weak_ptr {
|
, public base::has_weak_ptr {
|
||||||
public:
|
public:
|
||||||
CustomEmojiLoader(not_null<Session*> owner, const CustomEmojiId id);
|
CustomEmojiLoader(
|
||||||
|
not_null<Session*> owner,
|
||||||
|
const CustomEmojiId id,
|
||||||
|
SizeTag tag);
|
||||||
|
CustomEmojiLoader(not_null<DocumentData*> document, SizeTag tag);
|
||||||
|
|
||||||
[[nodiscard]] bool resolving() const;
|
[[nodiscard]] bool resolving() const;
|
||||||
void resolved(not_null<DocumentData*> document);
|
void resolved(not_null<DocumentData*> document);
|
||||||
|
|
||||||
void load(Fn<void(Ui::CustomEmoji::Caching)> ready) override;
|
QString entityData() override;
|
||||||
|
|
||||||
|
void load(Fn<void(LoadResult)> loaded) override;
|
||||||
|
bool loading() override;
|
||||||
void cancel() override;
|
void cancel() override;
|
||||||
Ui::CustomEmoji::Preview preview() override;
|
Ui::CustomEmoji::Preview preview() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Resolve {
|
struct Resolve {
|
||||||
Fn<void(Ui::CustomEmoji::Caching)> requested;
|
Fn<void(LoadResult)> requested;
|
||||||
|
QString entityData;
|
||||||
};
|
};
|
||||||
struct Process {
|
struct Process {
|
||||||
std::shared_ptr<DocumentMedia> media;
|
std::shared_ptr<DocumentMedia> media;
|
||||||
Fn<void(Ui::CustomEmoji::Caching)> callback;
|
Fn<void(LoadResult)> loaded;
|
||||||
|
base::has_weak_ptr guard;
|
||||||
rpl::lifetime lifetime;
|
rpl::lifetime lifetime;
|
||||||
};
|
};
|
||||||
struct Load {
|
struct Requested {
|
||||||
not_null<DocumentData*> document;
|
not_null<DocumentData*> document;
|
||||||
std::unique_ptr<Process> process;
|
std::unique_ptr<Process> process;
|
||||||
};
|
};
|
||||||
|
struct Lookup : Requested {
|
||||||
|
};
|
||||||
|
struct Load : Requested {
|
||||||
|
};
|
||||||
|
|
||||||
[[nodiscard]] static std::variant<Resolve, Load> InitialState(
|
void check();
|
||||||
|
[[nodiscard]] Storage::Cache::Key cacheKey(
|
||||||
|
not_null<DocumentData*> document) const;
|
||||||
|
void startCacheLookup(
|
||||||
|
not_null<Lookup*> lookup,
|
||||||
|
Fn<void(LoadResult)> loaded);
|
||||||
|
void lookupDone(
|
||||||
|
not_null<Lookup*> lookup,
|
||||||
|
std::optional<Ui::CustomEmoji::Cache> result);
|
||||||
|
void loadNoCache(
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
Fn<void(LoadResult)> loaded);
|
||||||
|
|
||||||
|
[[nodiscard]] static std::variant<Resolve, Lookup, Load> InitialState(
|
||||||
not_null<Session*> owner,
|
not_null<Session*> owner,
|
||||||
const CustomEmojiId &id);
|
const CustomEmojiId &id);
|
||||||
|
|
||||||
std::variant<Resolve, Load> _state;
|
std::variant<Resolve, Lookup, Load> _state;
|
||||||
|
SizeTag _tag = SizeTag::Normal;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
CustomEmojiLoader::CustomEmojiLoader(
|
CustomEmojiLoader::CustomEmojiLoader(
|
||||||
not_null<Session*> owner,
|
not_null<Session*> owner,
|
||||||
const CustomEmojiId id)
|
const CustomEmojiId id,
|
||||||
: _state(InitialState(owner, id)) {
|
SizeTag tag)
|
||||||
|
: _state(InitialState(owner, id))
|
||||||
|
, _tag(tag) {
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomEmojiLoader::CustomEmojiLoader(
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
SizeTag tag)
|
||||||
|
: _state(Lookup{ document })
|
||||||
|
, _tag(tag) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CustomEmojiLoader::resolving() const {
|
bool CustomEmojiLoader::resolving() const {
|
||||||
|
@ -180,41 +172,179 @@ void CustomEmojiLoader::resolved(not_null<DocumentData*> document) {
|
||||||
Expects(resolving());
|
Expects(resolving());
|
||||||
|
|
||||||
auto requested = std::move(v::get<Resolve>(_state).requested);
|
auto requested = std::move(v::get<Resolve>(_state).requested);
|
||||||
_state = Load{ document };
|
_state = Lookup{ document };
|
||||||
if (requested) {
|
if (requested) {
|
||||||
load(std::move(requested));
|
load(std::move(requested));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CustomEmojiLoader::load(Fn<void(Ui::CustomEmoji::Caching)> ready) {
|
void CustomEmojiLoader::load(Fn<void(LoadResult)> loaded) {
|
||||||
if (const auto resolve = std::get_if<Resolve>(&_state)) {
|
if (const auto resolve = std::get_if<Resolve>(&_state)) {
|
||||||
resolve->requested = std::move(ready);
|
resolve->requested = std::move(loaded);
|
||||||
|
} else if (const auto lookup = std::get_if<Lookup>(&_state)) {
|
||||||
|
if (!lookup->process) {
|
||||||
|
startCacheLookup(lookup, std::move(loaded));
|
||||||
|
} else {
|
||||||
|
lookup->process->loaded = std::move(loaded);
|
||||||
|
}
|
||||||
} else if (const auto load = std::get_if<Load>(&_state)) {
|
} else if (const auto load = std::get_if<Load>(&_state)) {
|
||||||
if (!load->process) {
|
if (!load->process) {
|
||||||
load->process = std::make_unique<Process>(Process{
|
load->process = std::make_unique<Process>(Process{
|
||||||
.media = load->document->createMediaView(),
|
.media = load->document->createMediaView(),
|
||||||
.callback = std::move(ready),
|
.loaded = std::move(loaded),
|
||||||
});
|
});
|
||||||
load->process->media->checkStickerLarge();
|
load->process->media->checkStickerLarge();
|
||||||
|
if (load->process->media->loaded()) {
|
||||||
|
check();
|
||||||
|
} else {
|
||||||
|
load->document->session().downloaderTaskFinished(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
check();
|
||||||
|
}, load->process->lifetime);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
load->process->callback = std::move(ready);
|
load->process->loaded = std::move(loaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString CustomEmojiLoader::entityData() {
|
||||||
|
if (const auto resolve = std::get_if<Resolve>(&_state)) {
|
||||||
|
return resolve->entityData;
|
||||||
|
} else if (const auto lookup = std::get_if<Lookup>(&_state)) {
|
||||||
|
return SerializeCustomEmojiId(lookup->document);
|
||||||
|
} else if (const auto load = std::get_if<Load>(&_state)) {
|
||||||
|
return SerializeCustomEmojiId(load->document);
|
||||||
|
}
|
||||||
|
Unexpected("State in CustomEmojiLoader::entityData.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CustomEmojiLoader::loading() {
|
||||||
|
if (const auto resolve = std::get_if<Resolve>(&_state)) {
|
||||||
|
return (resolve->requested != nullptr);
|
||||||
|
} else if (const auto lookup = std::get_if<Lookup>(&_state)) {
|
||||||
|
return (lookup->process != nullptr);
|
||||||
|
} else if (const auto load = std::get_if<Load>(&_state)) {
|
||||||
|
return (load->process != nullptr);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::Cache::Key CustomEmojiLoader::cacheKey(
|
||||||
|
not_null<DocumentData*> document) const {
|
||||||
|
const auto baseKey = document->bigFileBaseCacheKey();
|
||||||
|
if (!baseKey) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return Storage::Cache::Key{
|
||||||
|
baseKey.high,
|
||||||
|
baseKey.low + ChatHelpers::LottieCacheKeyShift(
|
||||||
|
0x0F,
|
||||||
|
LottieSizeFromTag(_tag)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomEmojiLoader::startCacheLookup(
|
||||||
|
not_null<Lookup*> lookup,
|
||||||
|
Fn<void(LoadResult)> loaded) {
|
||||||
|
const auto document = lookup->document;
|
||||||
|
const auto key = cacheKey(document);
|
||||||
|
if (!key) {
|
||||||
|
loadNoCache(document, std::move(loaded));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lookup->process = std::make_unique<Process>(Process{
|
||||||
|
.loaded = std::move(loaded),
|
||||||
|
});
|
||||||
|
const auto weak = base::make_weak(&lookup->process->guard);
|
||||||
|
document->owner().cacheBigFile().get(key, [=](QByteArray value) {
|
||||||
|
auto cache = Ui::CustomEmoji::Cache::FromSerialized(value);
|
||||||
|
crl::on_main(weak, [=, result = std::move(cache)]() mutable {
|
||||||
|
lookupDone(lookup, std::move(result));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomEmojiLoader::lookupDone(
|
||||||
|
not_null<Lookup*> lookup,
|
||||||
|
std::optional<Ui::CustomEmoji::Cache> result) {
|
||||||
|
const auto document = lookup->document;
|
||||||
|
if (!result) {
|
||||||
|
loadNoCache(document, std::move(lookup->process->loaded));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto tag = _tag;
|
||||||
|
auto loader = [=] {
|
||||||
|
return std::make_unique<CustomEmojiLoader>(document, tag);
|
||||||
|
};
|
||||||
|
lookup->process->loaded(Ui::CustomEmoji::Cached(
|
||||||
|
SerializeCustomEmojiId(document),
|
||||||
|
std::move(loader),
|
||||||
|
std::move(*result)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomEmojiLoader::loadNoCache(
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
Fn<void(LoadResult)> loaded) {
|
||||||
|
_state = Load{ document };
|
||||||
|
load(std::move(loaded));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomEmojiLoader::check() {
|
||||||
|
using namespace Ui::CustomEmoji;
|
||||||
|
|
||||||
|
const auto load = std::get_if<Load>(&_state);
|
||||||
|
Assert(load != nullptr);
|
||||||
|
Assert(load->process != nullptr);
|
||||||
|
|
||||||
|
const auto media = load->process->media.get();
|
||||||
|
const auto document = media->owner();
|
||||||
|
const auto data = media->bytes();
|
||||||
|
const auto filepath = document->filepath();
|
||||||
|
if (data.isEmpty() && filepath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
load->process->lifetime.destroy();
|
||||||
|
|
||||||
|
const auto tag = _tag;
|
||||||
|
const auto size = SizeFromTag(_tag);
|
||||||
|
auto bytes = Lottie::ReadContent(data, filepath);
|
||||||
|
auto loader = [=] {
|
||||||
|
return std::make_unique<CustomEmojiLoader>(document, tag);
|
||||||
|
};
|
||||||
|
auto put = [=, key = cacheKey(document)](QByteArray value) {
|
||||||
|
document->owner().cacheBigFile().put(key, std::move(value));
|
||||||
|
};
|
||||||
|
auto generator = [=, bytes = Lottie::ReadContent(data, filepath)]() {
|
||||||
|
return std::make_unique<Lottie::EmojiGenerator>(bytes);
|
||||||
|
};
|
||||||
|
auto renderer = std::make_unique<Renderer>(RendererDescriptor{
|
||||||
|
.generator = std::move(generator),
|
||||||
|
.put = std::move(put),
|
||||||
|
.loader = std::move(loader),
|
||||||
|
.size = SizeFromTag(_tag),
|
||||||
|
});
|
||||||
|
base::take(load->process)->loaded(Caching{
|
||||||
|
std::move(renderer),
|
||||||
|
SerializeCustomEmojiId(document),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
auto CustomEmojiLoader::InitialState(
|
auto CustomEmojiLoader::InitialState(
|
||||||
not_null<Session*> owner,
|
not_null<Session*> owner,
|
||||||
const CustomEmojiId &id)
|
const CustomEmojiId &id)
|
||||||
-> std::variant<Resolve, Load> {
|
-> std::variant<Resolve, Lookup, Load> {
|
||||||
const auto document = owner->document(id.id);
|
const auto document = owner->document(id.id);
|
||||||
if (!document->isNull()) {
|
if (!document->isNull()) {
|
||||||
return Load{ document };
|
return Lookup{ document };
|
||||||
}
|
}
|
||||||
return Resolve();
|
return Resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CustomEmojiLoader::cancel() {
|
void CustomEmojiLoader::cancel() {
|
||||||
if (const auto load = std::get_if<Load>(&_state)) {
|
if (const auto lookup = std::get_if<Lookup>(&_state)) {
|
||||||
|
base::take(lookup->process);
|
||||||
|
} else if (const auto load = std::get_if<Load>(&_state)) {
|
||||||
if (base::take(load->process)) {
|
if (base::take(load->process)) {
|
||||||
load->document->cancel();
|
load->document->cancel();
|
||||||
}
|
}
|
||||||
|
@ -222,22 +352,28 @@ void CustomEmojiLoader::cancel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ui::CustomEmoji::Preview CustomEmojiLoader::preview() {
|
Ui::CustomEmoji::Preview CustomEmojiLoader::preview() {
|
||||||
if (const auto load = std::get_if<Load>(&_state)) {
|
using Preview = Ui::CustomEmoji::Preview;
|
||||||
if (const auto process = load->process.get()) {
|
const auto make = [&](not_null<DocumentData*> document) -> Preview {
|
||||||
const auto dimensions = load->document->dimensions;
|
const auto dimensions = document->dimensions;
|
||||||
if (!dimensions.width()) {
|
if (!document->inlineThumbnailIsPath()
|
||||||
return {};
|
|| !dimensions.width()) {
|
||||||
}
|
return {};
|
||||||
const auto scale = (Ui::Emoji::GetSizeNormal() * 1.)
|
|
||||||
/ (style::DevicePixelRatio() * dimensions.width());
|
|
||||||
return { process->media->thumbnailPath(), scale };
|
|
||||||
}
|
}
|
||||||
|
const auto scale = (SizeFromTag(_tag) * 1.)
|
||||||
|
/ (style::DevicePixelRatio() * dimensions.width());
|
||||||
|
return { document->createMediaView()->thumbnailPath(), scale };
|
||||||
|
};
|
||||||
|
if (const auto lookup = std::get_if<Lookup>(&_state)) {
|
||||||
|
return make(lookup->document);
|
||||||
|
} else if (const auto load = std::get_if<Load>(&_state)) {
|
||||||
|
return make(load->document);
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomEmojiManager::CustomEmojiManager(not_null<Session*> owner)
|
CustomEmojiManager::CustomEmojiManager(not_null<Session*> owner)
|
||||||
: _owner(owner) {
|
: _owner(owner)
|
||||||
|
, _repaintTimer([=] { invokeRepaints(); }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomEmojiManager::~CustomEmojiManager() = default;
|
CustomEmojiManager::~CustomEmojiManager() = default;
|
||||||
|
@ -256,20 +392,30 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
|
||||||
auto j = i->second.find(parsed.id);
|
auto j = i->second.find(parsed.id);
|
||||||
if (j == end(i->second)) {
|
if (j == end(i->second)) {
|
||||||
using Loading = Ui::CustomEmoji::Loading;
|
using Loading = Ui::CustomEmoji::Loading;
|
||||||
auto loader = std::make_unique<CustomEmojiLoader>(_owner, parsed);
|
auto loader = std::make_unique<CustomEmojiLoader>(
|
||||||
|
_owner,
|
||||||
|
parsed,
|
||||||
|
SizeTag::Normal);
|
||||||
if (loader->resolving()) {
|
if (loader->resolving()) {
|
||||||
_loaders[parsed.id].push_back(base::make_weak(loader.get()));
|
_loaders[parsed.id].push_back(base::make_weak(loader.get()));
|
||||||
}
|
}
|
||||||
|
const auto repaint = [=](
|
||||||
|
not_null<Ui::CustomEmoji::Instance*> instance,
|
||||||
|
Ui::CustomEmoji::RepaintRequest request) {
|
||||||
|
repaintLater(instance, request);
|
||||||
|
};
|
||||||
j = i->second.emplace(
|
j = i->second.emplace(
|
||||||
parsed.id,
|
parsed.id,
|
||||||
std::make_unique<Ui::CustomEmoji::Instance>(data, Loading{
|
std::make_unique<Ui::CustomEmoji::Instance>(Loading{
|
||||||
std::move(loader),
|
std::move(loader),
|
||||||
Ui::CustomEmoji::Preview()
|
Ui::CustomEmoji::Preview()
|
||||||
})).first;
|
}, std::move(repaint))).first;
|
||||||
}
|
}
|
||||||
requestSetIfNeeded(parsed);
|
requestSetIfNeeded(parsed);
|
||||||
|
|
||||||
return std::make_unique<Ui::CustomEmoji::Object>(j->second.get());
|
return std::make_unique<Ui::CustomEmoji::Object>(
|
||||||
|
j->second.get(),
|
||||||
|
std::move(update));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
|
void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
|
||||||
|
@ -322,6 +468,64 @@ void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
|
||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CustomEmojiManager::repaintLater(
|
||||||
|
not_null<Ui::CustomEmoji::Instance*> instance,
|
||||||
|
Ui::CustomEmoji::RepaintRequest request) {
|
||||||
|
auto &bunch = _repaints[request.duration];
|
||||||
|
if (bunch.when < request.when) {
|
||||||
|
bunch.when = request.when;
|
||||||
|
}
|
||||||
|
bunch.instances.emplace_back(instance);
|
||||||
|
scheduleRepaintTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomEmojiManager::scheduleRepaintTimer() {
|
||||||
|
if (_repaintTimerScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_repaintTimerScheduled = true;
|
||||||
|
Ui::PostponeCall(this, [=] {
|
||||||
|
_repaintTimerScheduled = false;
|
||||||
|
|
||||||
|
auto next = crl::time();
|
||||||
|
for (const auto &[duration, bunch] : _repaints) {
|
||||||
|
if (!next || next > bunch.when) {
|
||||||
|
next = bunch.when;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (next && (!_repaintNext || _repaintNext > next)) {
|
||||||
|
const auto now = crl::now();
|
||||||
|
if (now >= next) {
|
||||||
|
_repaintNext = 0;
|
||||||
|
_repaintTimer.cancel();
|
||||||
|
invokeRepaints();
|
||||||
|
} else {
|
||||||
|
_repaintNext = next;
|
||||||
|
_repaintTimer.callOnce(next - now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomEmojiManager::invokeRepaints() {
|
||||||
|
_repaintNext = 0;
|
||||||
|
const auto now = crl::now();
|
||||||
|
for (auto i = begin(_repaints); i != end(_repaints);) {
|
||||||
|
if (i->second.when > now) {
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto bunch = std::move(i->second);
|
||||||
|
i = _repaints.erase(i);
|
||||||
|
for (const auto &weak : bunch.instances) {
|
||||||
|
if (const auto strong = weak.get()) {
|
||||||
|
strong->repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleRepaintTimer();
|
||||||
|
}
|
||||||
|
|
||||||
Main::Session &CustomEmojiManager::session() const {
|
Main::Session &CustomEmojiManager::session() const {
|
||||||
return _owner->session();
|
return _owner->session();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/text/custom_emoji_instance.h"
|
#include "ui/text/custom_emoji_instance.h"
|
||||||
|
#include "base/timer.h"
|
||||||
|
#include "base/weak_ptr.h"
|
||||||
|
|
||||||
struct StickerSetIdentifier;
|
struct StickerSetIdentifier;
|
||||||
|
|
||||||
|
@ -21,8 +23,13 @@ class Session;
|
||||||
struct CustomEmojiId;
|
struct CustomEmojiId;
|
||||||
class CustomEmojiLoader;
|
class CustomEmojiLoader;
|
||||||
|
|
||||||
class CustomEmojiManager final {
|
class CustomEmojiManager final : public base::has_weak_ptr {
|
||||||
public:
|
public:
|
||||||
|
enum class SizeTag {
|
||||||
|
Normal,
|
||||||
|
Large,
|
||||||
|
};
|
||||||
|
|
||||||
CustomEmojiManager(not_null<Session*> owner);
|
CustomEmojiManager(not_null<Session*> owner);
|
||||||
~CustomEmojiManager();
|
~CustomEmojiManager();
|
||||||
|
|
||||||
|
@ -40,8 +47,17 @@ private:
|
||||||
base::flat_set<uint64> documents;
|
base::flat_set<uint64> documents;
|
||||||
base::flat_set<uint64> waiting;
|
base::flat_set<uint64> waiting;
|
||||||
};
|
};
|
||||||
|
struct RepaintBunch {
|
||||||
|
crl::time when = 0;
|
||||||
|
std::vector<base::weak_ptr<Ui::CustomEmoji::Instance>> instances;
|
||||||
|
};
|
||||||
|
|
||||||
void requestSetIfNeeded(const CustomEmojiId &id);
|
void requestSetIfNeeded(const CustomEmojiId &id);
|
||||||
|
void repaintLater(
|
||||||
|
not_null<Ui::CustomEmoji::Instance*> instance,
|
||||||
|
Ui::CustomEmoji::RepaintRequest request);
|
||||||
|
void scheduleRepaintTimer();
|
||||||
|
void invokeRepaints();
|
||||||
|
|
||||||
const not_null<Session*> _owner;
|
const not_null<Session*> _owner;
|
||||||
|
|
||||||
|
@ -53,6 +69,11 @@ private:
|
||||||
uint64,
|
uint64,
|
||||||
std::vector<base::weak_ptr<CustomEmojiLoader>>> _loaders;
|
std::vector<base::weak_ptr<CustomEmojiLoader>>> _loaders;
|
||||||
|
|
||||||
|
base::flat_map<crl::time, RepaintBunch> _repaints;
|
||||||
|
crl::time _repaintNext = 0;
|
||||||
|
base::Timer _repaintTimer;
|
||||||
|
bool _repaintTimerScheduled = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void FillTestCustomEmoji(
|
void FillTestCustomEmoji(
|
||||||
|
|
|
@ -275,8 +275,12 @@ Message::Message(
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::~Message() {
|
Message::~Message() {
|
||||||
if (_comments) {
|
if (_comments || _heavyCustomEmoji) {
|
||||||
_comments = nullptr;
|
_comments = nullptr;
|
||||||
|
if (_heavyCustomEmoji) {
|
||||||
|
_heavyCustomEmoji = false;
|
||||||
|
message()->_text.unloadCustomEmoji();
|
||||||
|
}
|
||||||
checkHeavyPart();
|
checkHeavyPart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1241,8 +1245,20 @@ void Message::paintText(
|
||||||
const auto stm = context.messageStyle();
|
const auto stm = context.messageStyle();
|
||||||
p.setPen(stm->historyTextFg);
|
p.setPen(stm->historyTextFg);
|
||||||
p.setFont(st::msgFont);
|
p.setFont(st::msgFont);
|
||||||
item->_text.draw(p, trect.x(), trect.y(), trect.width(), style::al_left, 0, -1, context.selection);
|
const auto custom = item->_text.hasCustomEmoji();
|
||||||
if (!_heavyCustomEmoji && item->_text.hasCustomEmoji()) {
|
if (custom) {
|
||||||
|
p.setInactive(delegate()->elementIsGifPaused());
|
||||||
|
}
|
||||||
|
item->_text.draw(
|
||||||
|
p,
|
||||||
|
trect.x(),
|
||||||
|
trect.y(),
|
||||||
|
trect.width(),
|
||||||
|
style::al_left,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
context.selection);
|
||||||
|
if (!_heavyCustomEmoji && custom) {
|
||||||
_heavyCustomEmoji = true;
|
_heavyCustomEmoji = true;
|
||||||
history()->owner().registerHeavyViewPart(const_cast<Message*>(this));
|
history()->owner().registerHeavyViewPart(const_cast<Message*>(this));
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "ui/text/custom_emoji_instance.h"
|
#include "ui/text/custom_emoji_instance.h"
|
||||||
|
|
||||||
|
#include "ui/effects/frame_generator.h"
|
||||||
|
|
||||||
|
#include <crl/crl_async.h>
|
||||||
|
|
||||||
class QPainter;
|
class QPainter;
|
||||||
|
|
||||||
namespace Ui::CustomEmoji {
|
namespace Ui::CustomEmoji {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kMaxFrameDuration = 86400 * crl::time(1000);
|
||||||
|
|
||||||
|
struct CacheHelper {
|
||||||
|
int version = 0;
|
||||||
|
int size = 0;
|
||||||
|
int frames = 0;
|
||||||
|
int length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
Preview::Preview(QPainterPath path, float64 scale)
|
Preview::Preview(QPainterPath path, float64 scale)
|
||||||
: _data(ScaledPath{ std::move(path), scale }) {
|
: _data(ScaledPath{ std::move(path), scale }) {
|
||||||
|
@ -26,6 +42,10 @@ void Preview::paint(QPainter &p, int x, int y, const QColor &preview) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Preview::image() const {
|
||||||
|
return v::is<QImage>(_data);
|
||||||
|
}
|
||||||
|
|
||||||
void Preview::paintPath(
|
void Preview::paintPath(
|
||||||
QPainter &p,
|
QPainter &p,
|
||||||
int x,
|
int x,
|
||||||
|
@ -52,7 +72,15 @@ void Preview::paintPath(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache::Cache(QSize size) : _size(size) {
|
Cache::Cache(int size) : _size(size) {
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Cache> Cache::FromSerialized(const QByteArray &serialized) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Cache::serialize() {
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
int Cache::frames() const {
|
int Cache::frames() const {
|
||||||
|
@ -60,53 +88,247 @@ int Cache::frames() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage Cache::frame(int index) const {
|
QImage Cache::frame(int index) const {
|
||||||
return QImage();
|
Expects(index < _frames);
|
||||||
|
|
||||||
|
const auto row = index / kPerRow;
|
||||||
|
const auto inrow = index % kPerRow;
|
||||||
|
const auto bytes = _bytes[row].data() + inrow * frameByteSize();
|
||||||
|
const auto data = reinterpret_cast<const uchar*>(bytes);
|
||||||
|
return QImage(data, _size, _size, QImage::Format_ARGB32_Premultiplied);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Cache::size() const {
|
||||||
|
return _size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Preview Cache::makePreview() const {
|
||||||
|
Expects(_frames > 0);
|
||||||
|
|
||||||
|
auto image = frame(0);
|
||||||
|
image.detach();
|
||||||
|
return { std::move(image) };
|
||||||
}
|
}
|
||||||
|
|
||||||
void Cache::reserve(int frames) {
|
void Cache::reserve(int frames) {
|
||||||
|
const auto rows = (frames + kPerRow - 1) / kPerRow;
|
||||||
|
if (const auto add = rows - int(_bytes.size()); add > 0) {
|
||||||
|
_bytes.resize(rows);
|
||||||
|
for (auto e = end(_bytes), i = e - add; i != e; ++i) {
|
||||||
|
i->resize(kPerRow * frameByteSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_durations.reserve(frames);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cached::Cached(std::unique_ptr<Unloader> unloader, Cache cache)
|
int Cache::frameRowByteSize() const {
|
||||||
: _unloader(std::move(unloader))
|
return _size * 4;
|
||||||
, _cache(cache) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Cached::paint(QPainter &p, int x, int y) {
|
int Cache::frameByteSize() const {
|
||||||
p.drawImage(x, y, _cache.frame(0));
|
return _size * frameRowByteSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
Loading Cached::unload() {
|
void Cache::add(crl::time duration, const QImage &frame) {
|
||||||
return Loading(_unloader->unload(), Preview(_cache.frame(0)));
|
Expects(duration < kMaxFrameDuration);
|
||||||
|
Expects(frame.size() == QSize(_size, _size));
|
||||||
|
Expects(frame.format() == QImage::Format_ARGB32_Premultiplied);
|
||||||
|
|
||||||
|
const auto rowSize = frameRowByteSize();
|
||||||
|
const auto frameSize = frameByteSize();
|
||||||
|
const auto row = (_frames / kPerRow);
|
||||||
|
const auto inrow = (_frames % kPerRow);
|
||||||
|
const auto rows = row + 1;
|
||||||
|
while (_bytes.size() < rows) {
|
||||||
|
_bytes.emplace_back();
|
||||||
|
_bytes.back().resize(kPerRow * frameSize);
|
||||||
|
}
|
||||||
|
const auto perLine = frame.bytesPerLine();
|
||||||
|
auto dst = _bytes[row].data() + inrow * frameSize;
|
||||||
|
auto src = frame.constBits();
|
||||||
|
for (auto y = 0; y != _size; ++y) {
|
||||||
|
memcpy(dst, src, rowSize);
|
||||||
|
dst += rowSize;
|
||||||
|
src += perLine;
|
||||||
|
}
|
||||||
|
++_frames;
|
||||||
|
_durations.push_back(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Cacher::reserve(int frames) {
|
void Cache::finish() {
|
||||||
_cache.reserve(frames);
|
_finished = true;
|
||||||
}
|
if (_frame == _frames) {
|
||||||
|
_frame = 0;
|
||||||
void Cacher::add(crl::time duration, QImage frame) {
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache Cacher::takeCache() {
|
|
||||||
return std::move(_cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
Caching::Caching(std::unique_ptr<Cacher> cacher, Preview preview)
|
|
||||||
: _cacher(std::move(cacher))
|
|
||||||
, _preview(std::move(preview)) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void Caching::paint(QPainter &p, int x, int y, const QColor &preview) {
|
|
||||||
if (!_cacher->paint(p, x, y)) {
|
|
||||||
_preview.paint(p, x, y, preview);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Cached> Caching::ready() {
|
PaintFrameResult Cache::paintCurrentFrame(
|
||||||
return _cacher->ready();
|
QPainter &p,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
crl::time now) {
|
||||||
|
if (!_frames) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto finishes = now ? currentFrameFinishes() : 0;
|
||||||
|
if (finishes && now >= finishes) {
|
||||||
|
++_frame;
|
||||||
|
if (_finished && _frame == _frames) {
|
||||||
|
_frame = 0;
|
||||||
|
}
|
||||||
|
_shown = now;
|
||||||
|
} else if (!_shown) {
|
||||||
|
_shown = now;
|
||||||
|
}
|
||||||
|
p.drawImage(
|
||||||
|
QRect(x, y, _size, _size),
|
||||||
|
frame(std::min(_frame, _frames - 1)));
|
||||||
|
const auto next = currentFrameFinishes();
|
||||||
|
const auto duration = next ? (next - _shown) : 0;
|
||||||
|
return {
|
||||||
|
.painted = true,
|
||||||
|
.next = currentFrameFinishes(),
|
||||||
|
.duration = duration,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Loading Caching::cancel() {
|
int Cache::currentFrame() const {
|
||||||
return Loading(_cacher->cancel(), std::move(_preview));
|
return _frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
crl::time Cache::currentFrameFinishes() const {
|
||||||
|
if (!_shown || _frame >= _durations.size()) {
|
||||||
|
return 0;
|
||||||
|
} else if (const auto duration = _durations[_frame]) {
|
||||||
|
return _shown + duration;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cached::Cached(
|
||||||
|
const QString &entityData,
|
||||||
|
Fn<std::unique_ptr<Loader>()> unloader,
|
||||||
|
Cache cache)
|
||||||
|
: _unloader(std::move(unloader))
|
||||||
|
, _cache(std::move(cache))
|
||||||
|
, _entityData(entityData) {
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Cached::entityData() const {
|
||||||
|
return _entityData;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaintFrameResult Cached::paint(QPainter &p, int x, int y, crl::time now) {
|
||||||
|
return _cache.paintCurrentFrame(p, x, y, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading Cached::unload() {
|
||||||
|
return Loading(_unloader(), _cache.makePreview());
|
||||||
|
}
|
||||||
|
|
||||||
|
Renderer::Renderer(RendererDescriptor &&descriptor)
|
||||||
|
: _cache(descriptor.size)
|
||||||
|
, _put(std::move(descriptor.put))
|
||||||
|
, _loader(std::move(descriptor.loader)) {
|
||||||
|
Expects(_loader != nullptr);
|
||||||
|
|
||||||
|
const auto size = _cache.size();
|
||||||
|
const auto guard = base::make_weak(this);
|
||||||
|
crl::async([=, factory = std::move(descriptor.generator)]() mutable {
|
||||||
|
auto generator = factory();
|
||||||
|
auto rendered = generator->renderNext(
|
||||||
|
QImage(),
|
||||||
|
QSize(size, size) * style::DevicePixelRatio(),
|
||||||
|
Qt::KeepAspectRatio);
|
||||||
|
if (rendered.image.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
crl::on_main(guard, [
|
||||||
|
=,
|
||||||
|
frame = std::move(rendered),
|
||||||
|
generator = std::move(generator)
|
||||||
|
]() mutable {
|
||||||
|
frameReady(
|
||||||
|
std::move(generator),
|
||||||
|
frame.duration,
|
||||||
|
std::move(frame.image));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::frameReady(
|
||||||
|
std::unique_ptr<Ui::FrameGenerator> generator,
|
||||||
|
crl::time duration,
|
||||||
|
QImage frame) {
|
||||||
|
if (frame.isNull()) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto count = generator->count()) {
|
||||||
|
if (!_cache.frames()) {
|
||||||
|
_cache.reserve(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto explicitRepaint = (_cache.frames() == _cache.currentFrame());
|
||||||
|
_cache.add(duration, frame);
|
||||||
|
const auto size = _cache.size();
|
||||||
|
const auto guard = base::make_weak(this);
|
||||||
|
crl::async([
|
||||||
|
=,
|
||||||
|
frame = std::move(frame),
|
||||||
|
generator = std::move(generator)
|
||||||
|
]() mutable {
|
||||||
|
auto rendered = generator->renderNext(
|
||||||
|
std::move(frame),
|
||||||
|
QSize(size, size) * style::DevicePixelRatio(),
|
||||||
|
Qt::KeepAspectRatio);
|
||||||
|
crl::on_main(guard, [
|
||||||
|
=,
|
||||||
|
frame = std::move(rendered),
|
||||||
|
generator = std::move(generator)
|
||||||
|
]() mutable {
|
||||||
|
frameReady(
|
||||||
|
std::move(generator),
|
||||||
|
frame.duration,
|
||||||
|
std::move(frame.image));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (explicitRepaint && _repaint) {
|
||||||
|
_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::finish() {
|
||||||
|
_finished = true;
|
||||||
|
_cache.finish();
|
||||||
|
if (_put) {
|
||||||
|
_put(_cache.serialize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaintFrameResult Renderer::paint(QPainter &p, int x, int y, crl::time now) {
|
||||||
|
return _cache.paintCurrentFrame(p, x, y, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Cached> Renderer::ready(const QString &entityData) {
|
||||||
|
return _finished
|
||||||
|
? Cached{ entityData, std::move(_loader), std::move(_cache) }
|
||||||
|
: std::optional<Cached>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Loader> Renderer::cancel() {
|
||||||
|
return _loader();
|
||||||
|
}
|
||||||
|
|
||||||
|
Preview Renderer::makePreview() const {
|
||||||
|
return _cache.makePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::setRepaintCallback(Fn<void()> repaint) {
|
||||||
|
_repaint = std::move(repaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache Renderer::takeCache() {
|
||||||
|
return std::move(_cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
|
Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
|
||||||
|
@ -114,8 +336,24 @@ Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
|
||||||
, _preview(std::move(preview)) {
|
, _preview(std::move(preview)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Loading::load(Fn<void(Caching)> done) {
|
QString Loading::entityData() const {
|
||||||
_loader->load(crl::guard(this, std::move(done)));
|
return _loader->entityData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Loading::load(Fn<void(Loader::LoadResult)> done) {
|
||||||
|
_loader->load(crl::guard(this, [this, done = std::move(done)](
|
||||||
|
Loader::LoadResult result) mutable {
|
||||||
|
if (const auto caching = std::get_if<Caching>(&result)) {
|
||||||
|
caching->preview = _preview
|
||||||
|
? std::move(_preview)
|
||||||
|
: _loader->preview();
|
||||||
|
}
|
||||||
|
done(std::move(result));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Loading::loading() const {
|
||||||
|
return _loader->loading();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Loading::paint(QPainter &p, int x, int y, const QColor &preview) {
|
void Loading::paint(QPainter &p, int x, int y, const QColor &preview) {
|
||||||
|
@ -132,52 +370,97 @@ void Loading::cancel() {
|
||||||
invalidate_weak_ptrs(this);
|
invalidate_weak_ptrs(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance::Instance(const QString &entityData, Loading loading)
|
Instance::Instance(
|
||||||
|
Loading loading,
|
||||||
|
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater)
|
||||||
: _state(std::move(loading))
|
: _state(std::move(loading))
|
||||||
, _entityData(entityData) {
|
, _repaintLater(std::move(repaintLater)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Instance::entityData() const {
|
QString Instance::entityData() const {
|
||||||
return _entityData;
|
if (const auto loading = std::get_if<Loading>(&_state)) {
|
||||||
|
return loading->entityData();
|
||||||
|
} else if (const auto caching = std::get_if<Caching>(&_state)) {
|
||||||
|
return caching->entityData;
|
||||||
|
} else if (const auto cached = std::get_if<Cached>(&_state)) {
|
||||||
|
return cached->entityData();
|
||||||
|
}
|
||||||
|
Unexpected("State in Instance::entityData.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Instance::paint(QPainter &p, int x, int y, const QColor &preview) {
|
void Instance::paint(
|
||||||
|
QPainter &p,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
crl::time now,
|
||||||
|
const QColor &preview,
|
||||||
|
bool paused) {
|
||||||
if (const auto loading = std::get_if<Loading>(&_state)) {
|
if (const auto loading = std::get_if<Loading>(&_state)) {
|
||||||
loading->paint(p, x, y, preview);
|
loading->paint(p, x, y, preview);
|
||||||
loading->load([=](Caching caching) {
|
loading->load([=](Loader::LoadResult result) {
|
||||||
_state = std::move(caching);
|
if (auto caching = std::get_if<Caching>(&result)) {
|
||||||
|
caching->renderer->setRepaintCallback([=] { repaint(); });
|
||||||
|
_state = std::move(*caching);
|
||||||
|
} else if (auto cached = std::get_if<Cached>(&result)) {
|
||||||
|
_state = std::move(*cached);
|
||||||
|
} else {
|
||||||
|
Unexpected("Value in Loader::LoadResult.");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (const auto caching = std::get_if<Caching>(&_state)) {
|
} else if (const auto caching = std::get_if<Caching>(&_state)) {
|
||||||
caching->paint(p, x, y, preview);
|
auto result = caching->renderer->paint(p, x, y, paused ? 0 : now);
|
||||||
if (auto cached = caching->ready()) {
|
if (!result.painted) {
|
||||||
|
caching->preview.paint(p, x, y, preview);
|
||||||
|
} else {
|
||||||
|
if (!caching->preview.image()) {
|
||||||
|
caching->preview = caching->renderer->makePreview();
|
||||||
|
}
|
||||||
|
if (result.next > now) {
|
||||||
|
_repaintLater(this, { result.next, result.duration });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto cached = caching->renderer->ready(caching->entityData)) {
|
||||||
_state = std::move(*cached);
|
_state = std::move(*cached);
|
||||||
}
|
}
|
||||||
} else if (const auto cached = std::get_if<Cached>(&_state)) {
|
} else if (const auto cached = std::get_if<Cached>(&_state)) {
|
||||||
cached->paint(p, x, y);
|
const auto result = cached->paint(p, x, y, paused ? 0 : now);
|
||||||
|
if (result.next > now) {
|
||||||
|
_repaintLater(this, { result.next, result.duration });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Instance::incrementUsage() {
|
void Instance::repaint() {
|
||||||
++_usage;
|
for (const auto &object : _usage) {
|
||||||
|
object->repaint();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Instance::decrementUsage() {
|
void Instance::incrementUsage(not_null<Object*> object) {
|
||||||
Expects(_usage > 0);
|
_usage.emplace(object);
|
||||||
|
}
|
||||||
|
|
||||||
if (--_usage > 0) {
|
void Instance::decrementUsage(not_null<Object*> object) {
|
||||||
|
_usage.remove(object);
|
||||||
|
if (!_usage.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (const auto loading = std::get_if<Loading>(&_state)) {
|
if (const auto loading = std::get_if<Loading>(&_state)) {
|
||||||
loading->cancel();
|
loading->cancel();
|
||||||
} else if (const auto caching = std::get_if<Caching>(&_state)) {
|
} else if (const auto caching = std::get_if<Caching>(&_state)) {
|
||||||
_state = caching->cancel();
|
_state = Loading{
|
||||||
|
caching->renderer->cancel(),
|
||||||
|
std::move(caching->preview),
|
||||||
|
};
|
||||||
} else if (const auto cached = std::get_if<Cached>(&_state)) {
|
} else if (const auto cached = std::get_if<Cached>(&_state)) {
|
||||||
_state = cached->unload();
|
_state = cached->unload();
|
||||||
}
|
}
|
||||||
|
_repaintLater(this, RepaintRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
Object::Object(not_null<Instance*> instance)
|
Object::Object(not_null<Instance*> instance, Fn<void()> repaint)
|
||||||
: _instance(instance) {
|
: _instance(instance)
|
||||||
|
, _repaint(std::move(repaint)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Object::~Object() {
|
Object::~Object() {
|
||||||
|
@ -188,19 +471,29 @@ QString Object::entityData() {
|
||||||
return _instance->entityData();
|
return _instance->entityData();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Object::paint(QPainter &p, int x, int y, const QColor &preview) {
|
void Object::paint(
|
||||||
|
QPainter &p,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
crl::time now,
|
||||||
|
const QColor &preview,
|
||||||
|
bool paused) {
|
||||||
if (!_using) {
|
if (!_using) {
|
||||||
_using = true;
|
_using = true;
|
||||||
_instance->incrementUsage();
|
_instance->incrementUsage(this);
|
||||||
}
|
}
|
||||||
_instance->paint(p, x, y, preview);
|
_instance->paint(p, x, y, now, preview, paused);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Object::unload() {
|
void Object::unload() {
|
||||||
if (_using) {
|
if (_using) {
|
||||||
_using = false;
|
_using = false;
|
||||||
_instance->decrementUsage();
|
_instance->decrementUsage(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Object::repaint() {
|
||||||
|
_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Ui::CustomEmoji
|
} // namespace Ui::CustomEmoji
|
||||||
|
|
|
@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
class QColor;
|
class QColor;
|
||||||
class QPainter;
|
class QPainter;
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class FrameGenerator;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
namespace Ui::CustomEmoji {
|
namespace Ui::CustomEmoji {
|
||||||
|
|
||||||
class Preview final {
|
class Preview final {
|
||||||
|
@ -23,6 +27,7 @@ public:
|
||||||
Preview(QPainterPath path, float64 scale);
|
Preview(QPainterPath path, float64 scale);
|
||||||
|
|
||||||
void paint(QPainter &p, int x, int y, const QColor &preview);
|
void paint(QPainter &p, int x, int y, const QColor &preview);
|
||||||
|
[[nodiscard]] bool image() const;
|
||||||
|
|
||||||
[[nodiscard]] explicit operator bool() const {
|
[[nodiscard]] explicit operator bool() const {
|
||||||
return !v::is_null(_data);
|
return !v::is_null(_data);
|
||||||
|
@ -45,81 +50,124 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PaintFrameResult {
|
||||||
|
bool painted = false;
|
||||||
|
crl::time next = 0;
|
||||||
|
crl::time duration = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class Cache final {
|
class Cache final {
|
||||||
public:
|
public:
|
||||||
Cache(QSize size);
|
Cache(int size);
|
||||||
|
|
||||||
|
[[nodiscard]] static std::optional<Cache> FromSerialized(
|
||||||
|
const QByteArray &serialized);
|
||||||
|
[[nodiscard]] QByteArray serialize();
|
||||||
|
|
||||||
|
[[nodiscard]] int size() const;
|
||||||
[[nodiscard]] int frames() const;
|
[[nodiscard]] int frames() const;
|
||||||
[[nodiscard]] QImage frame(int index) const;
|
[[nodiscard]] QImage frame(int index) const;
|
||||||
void reserve(int frames);
|
void reserve(int frames);
|
||||||
|
void add(crl::time duration, const QImage &frame);
|
||||||
|
void finish();
|
||||||
|
|
||||||
|
[[nodiscard]] Preview makePreview() const;
|
||||||
|
|
||||||
|
PaintFrameResult paintCurrentFrame(
|
||||||
|
QPainter &p,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
crl::time now);
|
||||||
|
[[nodiscard]] int currentFrame() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr auto kPerRow = 30;
|
static constexpr auto kPerRow = 16;
|
||||||
|
|
||||||
|
[[nodiscard]] int frameRowByteSize() const;
|
||||||
|
[[nodiscard]] int frameByteSize() const;
|
||||||
|
[[nodiscard]] crl::time currentFrameFinishes() const;
|
||||||
|
|
||||||
std::vector<bytes::vector> _bytes;
|
std::vector<bytes::vector> _bytes;
|
||||||
std::vector<int> _durations;
|
std::vector<int> _durations;
|
||||||
QSize _size;
|
crl::time _shown = 0;
|
||||||
|
int _frame = 0;
|
||||||
|
int _size = 0;
|
||||||
int _frames = 0;
|
int _frames = 0;
|
||||||
|
bool _finished = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Loader;
|
class Loader;
|
||||||
class Loading;
|
class Loading;
|
||||||
|
|
||||||
class Unloader {
|
|
||||||
public:
|
|
||||||
[[nodiscard]] virtual std::unique_ptr<Loader> unload() = 0;
|
|
||||||
virtual ~Unloader() = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Cached final {
|
class Cached final {
|
||||||
public:
|
public:
|
||||||
Cached(std::unique_ptr<Unloader> unloader, Cache cache);
|
Cached(
|
||||||
|
const QString &entityData,
|
||||||
|
Fn<std::unique_ptr<Loader>()> unloader,
|
||||||
|
Cache cache);
|
||||||
|
|
||||||
void paint(QPainter &p, int x, int y);
|
[[nodiscard]] QString entityData() const;
|
||||||
|
|
||||||
|
PaintFrameResult paint(QPainter &p, int x, int y, crl::time now);
|
||||||
[[nodiscard]] Loading unload();
|
[[nodiscard]] Loading unload();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<Unloader> _unloader;
|
Fn<std::unique_ptr<Loader>()> _unloader;
|
||||||
Cache _cache;
|
Cache _cache;
|
||||||
|
QString _entityData;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Cacher {
|
struct RendererDescriptor {
|
||||||
|
Fn<std::unique_ptr<Ui::FrameGenerator>()> generator;
|
||||||
|
Fn<void(QByteArray)> put;
|
||||||
|
Fn<std::unique_ptr<Loader>()> loader;
|
||||||
|
int size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Renderer final : public base::has_weak_ptr {
|
||||||
public:
|
public:
|
||||||
virtual bool paint(QPainter &p, int x, int y) = 0;
|
explicit Renderer(RendererDescriptor &&descriptor);
|
||||||
[[nodiscard]] virtual std::optional<Cached> ready() = 0;
|
virtual ~Renderer() = default;
|
||||||
[[nodiscard]] virtual std::unique_ptr<Loader> cancel() = 0;
|
|
||||||
virtual ~Cacher() = default;
|
|
||||||
|
|
||||||
protected:
|
PaintFrameResult paint(QPainter &p, int x, int y, crl::time now);
|
||||||
void reserve(int frames);
|
[[nodiscard]] std::optional<Cached> ready(const QString &entityData);
|
||||||
void add(crl::time duration, QImage frame);
|
[[nodiscard]] std::unique_ptr<Loader> cancel();
|
||||||
|
|
||||||
|
[[nodiscard]] Preview makePreview() const;
|
||||||
|
|
||||||
|
void setRepaintCallback(Fn<void()> repaint);
|
||||||
[[nodiscard]] Cache takeCache();
|
[[nodiscard]] Cache takeCache();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void frameReady(
|
||||||
|
std::unique_ptr<Ui::FrameGenerator> generator,
|
||||||
|
crl::time duration,
|
||||||
|
QImage frame);
|
||||||
|
void finish();
|
||||||
|
|
||||||
Cache _cache;
|
Cache _cache;
|
||||||
|
std::unique_ptr<Ui::FrameGenerator> _generator;
|
||||||
|
Fn<void(QByteArray)> _put;
|
||||||
|
Fn<void()> _repaint;
|
||||||
|
Fn<std::unique_ptr<Loader>()> _loader;
|
||||||
|
bool _finished = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Caching final {
|
struct Caching {
|
||||||
public:
|
std::unique_ptr<Renderer> renderer;
|
||||||
Caching(std::unique_ptr<Cacher> cacher, Preview preview);
|
QString entityData;
|
||||||
void paint(QPainter &p, int x, int y, const QColor &preview);
|
Preview preview;
|
||||||
|
|
||||||
[[nodiscard]] std::optional<Cached> ready();
|
|
||||||
[[nodiscard]] Loading cancel();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unique_ptr<Cacher> _cacher;
|
|
||||||
Preview _preview;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Loader {
|
class Loader {
|
||||||
public:
|
public:
|
||||||
virtual void load(Fn<void(Caching)> ready) = 0;
|
using LoadResult = std::variant<Caching, Cached>;
|
||||||
|
[[nodiscard]] virtual QString entityData() = 0;
|
||||||
|
virtual void load(Fn<void(LoadResult)> loaded) = 0;
|
||||||
|
[[nodiscard]] virtual bool loading() = 0;
|
||||||
virtual void cancel() = 0;
|
virtual void cancel() = 0;
|
||||||
[[nodiscard]] virtual Preview preview() = 0;
|
[[nodiscard]] virtual Preview preview() = 0;
|
||||||
virtual ~Loader() = default;
|
virtual ~Loader() = default;
|
||||||
|
@ -129,7 +177,10 @@ class Loading final : public base::has_weak_ptr {
|
||||||
public:
|
public:
|
||||||
Loading(std::unique_ptr<Loader> loader, Preview preview);
|
Loading(std::unique_ptr<Loader> loader, Preview preview);
|
||||||
|
|
||||||
void load(Fn<void(Caching)> done);
|
[[nodiscard]] QString entityData() const;
|
||||||
|
|
||||||
|
void load(Fn<void(Loader::LoadResult)> done);
|
||||||
|
[[nodiscard]] bool loading() const;
|
||||||
void paint(QPainter &p, int x, int y, const QColor &preview);
|
void paint(QPainter &p, int x, int y, const QColor &preview);
|
||||||
void cancel();
|
void cancel();
|
||||||
|
|
||||||
|
@ -139,21 +190,36 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Instance final {
|
struct RepaintRequest {
|
||||||
|
crl::time when = 0;
|
||||||
|
crl::time duration = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Object;
|
||||||
|
class Instance final : public base::has_weak_ptr {
|
||||||
public:
|
public:
|
||||||
Instance(const QString &entityData, Loading loading);
|
Instance(
|
||||||
|
Loading loading,
|
||||||
|
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater);
|
||||||
|
|
||||||
[[nodiscard]] QString entityData() const;
|
[[nodiscard]] QString entityData() const;
|
||||||
void paint(QPainter &p, int x, int y, const QColor &preview);
|
void paint(
|
||||||
|
QPainter &p,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
crl::time now,
|
||||||
|
const QColor &preview,
|
||||||
|
bool paused);
|
||||||
|
|
||||||
void incrementUsage();
|
void incrementUsage(not_null<Object*> object);
|
||||||
void decrementUsage();
|
void decrementUsage(not_null<Object*> object);
|
||||||
|
|
||||||
|
void repaint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::variant<Loading, Caching, Cached> _state;
|
std::variant<Loading, Caching, Cached> _state;
|
||||||
QString _entityData;
|
base::flat_set<not_null<Object*>> _usage;
|
||||||
|
Fn<void(not_null<Instance*> that, RepaintRequest)> _repaintLater;
|
||||||
int _usage = 0;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -165,15 +231,24 @@ public:
|
||||||
|
|
||||||
class Object final : public Ui::Text::CustomEmoji {
|
class Object final : public Ui::Text::CustomEmoji {
|
||||||
public:
|
public:
|
||||||
Object(not_null<Instance*> instance);
|
Object(not_null<Instance*> instance, Fn<void()> repaint);
|
||||||
~Object();
|
~Object();
|
||||||
|
|
||||||
QString entityData() override;
|
QString entityData() override;
|
||||||
void paint(QPainter &p, int x, int y, const QColor &preview) override;
|
void paint(
|
||||||
|
QPainter &p,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
crl::time now,
|
||||||
|
const QColor &preview,
|
||||||
|
bool paused) override;
|
||||||
void unload() override;
|
void unload() override;
|
||||||
|
|
||||||
|
void repaint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const not_null<Instance*> _instance;
|
const not_null<Instance*> _instance;
|
||||||
|
Fn<void()> _repaint;
|
||||||
bool _using = false;
|
bool _using = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -841,6 +841,7 @@ void SessionController::setupPremiumToast() {
|
||||||
}) | rpl::distinct_until_changed() | rpl::skip(
|
}) | rpl::distinct_until_changed() | rpl::skip(
|
||||||
1
|
1
|
||||||
) | rpl::filter([=](bool premium) {
|
) | rpl::filter([=](bool premium) {
|
||||||
|
session().mtp().requestConfig();
|
||||||
return premium;
|
return premium;
|
||||||
}) | rpl::start_with_next([=] {
|
}) | rpl::start_with_next([=] {
|
||||||
Ui::Toast::Show(
|
Ui::Toast::Show(
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7ae2b9480ef61e222fd46e7806e46875d523502c
|
Subproject commit 301708287d619c0ba01058d151d46aba2a15ec64
|
|
@ -1 +1 @@
|
||||||
Subproject commit 187110f43820a70d20b77e4063286b692d53294d
|
Subproject commit 6ef5ec3410ef5db33cb6413a7f05cc87a2c970bd
|
Loading…
Add table
Reference in a new issue