606 lines
14 KiB
C++
606 lines
14 KiB
C++
/*
|
|
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 "chat_helpers/stickers_emoji_pack.h"
|
|
|
|
#include "history/history_item.h"
|
|
#include "lottie/lottie_common.h"
|
|
#include "ui/emoji_config.h"
|
|
#include "ui/text/text_isolated_emoji.h"
|
|
#include "ui/image/image_source.h"
|
|
#include "main/main_session.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_document.h"
|
|
#include "apiwrap.h"
|
|
#include "styles/style_history.h"
|
|
|
|
namespace Stickers {
|
|
namespace details {
|
|
|
|
using UniversalImages = Ui::Emoji::UniversalImages;
|
|
|
|
class EmojiImageLoader {
|
|
public:
|
|
EmojiImageLoader(
|
|
crl::weak_on_queue<EmojiImageLoader> weak,
|
|
std::shared_ptr<UniversalImages> images,
|
|
bool largeEnabled);
|
|
|
|
[[nodiscard]] QImage prepare(EmojiPtr emoji);
|
|
void switchTo(std::shared_ptr<UniversalImages> images);
|
|
std::shared_ptr<UniversalImages> releaseImages();
|
|
|
|
private:
|
|
crl::weak_on_queue<EmojiImageLoader> _weak;
|
|
std::shared_ptr<UniversalImages> _images;
|
|
|
|
};
|
|
|
|
namespace {
|
|
|
|
constexpr auto kRefreshTimeout = TimeId(7200);
|
|
constexpr auto kClearSourceTimeout = 10 * crl::time(1000);
|
|
|
|
[[nodiscard]] QSize SingleSize() {
|
|
const auto single = st::largeEmojiSize;
|
|
const auto outline = st::largeEmojiOutline;
|
|
return QSize(
|
|
2 * outline + single,
|
|
2 * outline + single
|
|
) * cIntRetinaFactor();
|
|
}
|
|
|
|
[[nodiscard]] const Lottie::ColorReplacements *ColorReplacements(int index) {
|
|
Expects(index >= 1 && index <= 5);
|
|
|
|
static const auto color1 = Lottie::ColorReplacements{
|
|
{
|
|
{ 0xf77e41U, 0xca907aU },
|
|
{ 0xffb139U, 0xedc5a5U },
|
|
{ 0xffd140U, 0xf7e3c3U },
|
|
{ 0xffdf79U, 0xfbefd6U },
|
|
},
|
|
1,
|
|
};
|
|
static const auto color2 = Lottie::ColorReplacements{
|
|
{
|
|
{ 0xf77e41U, 0xaa7c60U },
|
|
{ 0xffb139U, 0xc8a987U },
|
|
{ 0xffd140U, 0xddc89fU },
|
|
{ 0xffdf79U, 0xe6d6b2U },
|
|
},
|
|
2,
|
|
};
|
|
static const auto color3 = Lottie::ColorReplacements{
|
|
{
|
|
{ 0xf77e41U, 0x8c6148U },
|
|
{ 0xffb139U, 0xad8562U },
|
|
{ 0xffd140U, 0xc49e76U },
|
|
{ 0xffdf79U, 0xd4b188U },
|
|
},
|
|
3,
|
|
};
|
|
static const auto color4 = Lottie::ColorReplacements{
|
|
{
|
|
{ 0xf77e41U, 0x6e3c2cU },
|
|
{ 0xffb139U, 0x925a34U },
|
|
{ 0xffd140U, 0xa16e46U },
|
|
{ 0xffdf79U, 0xac7a52U },
|
|
},
|
|
4,
|
|
};
|
|
static const auto color5 = Lottie::ColorReplacements{
|
|
{
|
|
{ 0xf77e41U, 0x291c12U },
|
|
{ 0xffb139U, 0x472a22U },
|
|
{ 0xffd140U, 0x573b30U },
|
|
{ 0xffdf79U, 0x68493cU },
|
|
},
|
|
5,
|
|
};
|
|
static const auto list = std::array{
|
|
&color1,
|
|
&color2,
|
|
&color3,
|
|
&color4,
|
|
&color5,
|
|
};
|
|
return list[index - 1];
|
|
}
|
|
|
|
class ImageSource : public Images::Source {
|
|
public:
|
|
explicit ImageSource(
|
|
EmojiPtr emoji,
|
|
not_null<crl::object_on_queue<EmojiImageLoader>*> loader);
|
|
|
|
void load(Data::FileOrigin origin) override;
|
|
void loadEvenCancelled(Data::FileOrigin origin) override;
|
|
QImage takeLoaded() override;
|
|
void unload() override;
|
|
|
|
void automaticLoad(
|
|
Data::FileOrigin origin,
|
|
const HistoryItem *item) override;
|
|
void automaticLoadSettingsChanged() override;
|
|
|
|
bool loading() override;
|
|
bool displayLoading() override;
|
|
void cancel() override;
|
|
float64 progress() override;
|
|
int loadOffset() override;
|
|
|
|
const StorageImageLocation &location() override;
|
|
void refreshFileReference(const QByteArray &data) override;
|
|
std::optional<Storage::Cache::Key> cacheKey() override;
|
|
void setDelayedStorageLocation(
|
|
const StorageImageLocation &location) override;
|
|
void performDelayedLoad(Data::FileOrigin origin) override;
|
|
bool isDelayedStorageImage() const override;
|
|
void setImageBytes(const QByteArray &bytes) override;
|
|
|
|
int width() override;
|
|
int height() override;
|
|
int bytesSize() override;
|
|
void setInformation(int size, int width, int height) override;
|
|
|
|
QByteArray bytesForCache() override;
|
|
|
|
private:
|
|
// While HistoryView::Element-s are almost never destroyed
|
|
// we make loading of the image lazy.
|
|
not_null<crl::object_on_queue<EmojiImageLoader>*> _loader;
|
|
EmojiPtr _emoji = nullptr;
|
|
QImage _data;
|
|
QByteArray _format;
|
|
QByteArray _bytes;
|
|
QSize _size;
|
|
base::binary_guard _loading;
|
|
|
|
};
|
|
|
|
ImageSource::ImageSource(
|
|
EmojiPtr emoji,
|
|
not_null<crl::object_on_queue<EmojiImageLoader>*> loader)
|
|
: _loader(loader)
|
|
, _emoji(emoji)
|
|
, _size(SingleSize()) {
|
|
}
|
|
|
|
void ImageSource::load(Data::FileOrigin origin) {
|
|
if (!_data.isNull()) {
|
|
return;
|
|
}
|
|
if (_bytes.isEmpty()) {
|
|
_loader->with([
|
|
this,
|
|
emoji = _emoji,
|
|
guard = _loading.make_guard()
|
|
](EmojiImageLoader &loader) mutable {
|
|
if (!guard) {
|
|
return;
|
|
}
|
|
crl::on_main(std::move(guard), [this, image = loader.prepare(emoji)]{
|
|
_data = image;
|
|
Auth().downloaderTaskFinished().notify();
|
|
});
|
|
});
|
|
} else {
|
|
_data = App::readImage(_bytes, &_format, false);
|
|
}
|
|
}
|
|
|
|
void ImageSource::loadEvenCancelled(Data::FileOrigin origin) {
|
|
load(origin);
|
|
}
|
|
|
|
QImage ImageSource::takeLoaded() {
|
|
load({});
|
|
return _data;
|
|
}
|
|
|
|
void ImageSource::unload() {
|
|
if (_bytes.isEmpty() && !_data.isNull()) {
|
|
if (_format != "JPG") {
|
|
_format = "PNG";
|
|
}
|
|
{
|
|
QBuffer buffer(&_bytes);
|
|
_data.save(&buffer, _format);
|
|
}
|
|
Assert(!_bytes.isEmpty());
|
|
}
|
|
_data = QImage();
|
|
}
|
|
|
|
void ImageSource::automaticLoad(
|
|
Data::FileOrigin origin,
|
|
const HistoryItem *item) {
|
|
}
|
|
|
|
void ImageSource::automaticLoadSettingsChanged() {
|
|
}
|
|
|
|
bool ImageSource::loading() {
|
|
return _data.isNull() && _bytes.isEmpty();
|
|
}
|
|
|
|
bool ImageSource::displayLoading() {
|
|
return false;
|
|
}
|
|
|
|
void ImageSource::cancel() {
|
|
}
|
|
|
|
float64 ImageSource::progress() {
|
|
return 1.;
|
|
}
|
|
|
|
int ImageSource::loadOffset() {
|
|
return 0;
|
|
}
|
|
|
|
const StorageImageLocation &ImageSource::location() {
|
|
return StorageImageLocation::Invalid();
|
|
}
|
|
|
|
void ImageSource::refreshFileReference(const QByteArray &data) {
|
|
}
|
|
|
|
std::optional<Storage::Cache::Key> ImageSource::cacheKey() {
|
|
return std::nullopt;
|
|
}
|
|
|
|
void ImageSource::setDelayedStorageLocation(
|
|
const StorageImageLocation &location) {
|
|
}
|
|
|
|
void ImageSource::performDelayedLoad(Data::FileOrigin origin) {
|
|
}
|
|
|
|
bool ImageSource::isDelayedStorageImage() const {
|
|
return false;
|
|
}
|
|
|
|
void ImageSource::setImageBytes(const QByteArray &bytes) {
|
|
}
|
|
|
|
int ImageSource::width() {
|
|
return _size.width();
|
|
}
|
|
|
|
int ImageSource::height() {
|
|
return _size.height();
|
|
}
|
|
|
|
int ImageSource::bytesSize() {
|
|
return _bytes.size();
|
|
}
|
|
|
|
void ImageSource::setInformation(int size, int width, int height) {
|
|
if (width && height) {
|
|
_size = QSize(width, height);
|
|
}
|
|
}
|
|
|
|
QByteArray ImageSource::bytesForCache() {
|
|
auto result = QByteArray();
|
|
{
|
|
QBuffer buffer(&result);
|
|
if (!_data.save(&buffer, _format)) {
|
|
if (_data.save(&buffer, "PNG")) {
|
|
_format = "PNG";
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
EmojiImageLoader::EmojiImageLoader(
|
|
crl::weak_on_queue<EmojiImageLoader> weak,
|
|
std::shared_ptr<UniversalImages> images,
|
|
bool largeEnabled)
|
|
: _weak(std::move(weak))
|
|
, _images(std::move(images)) {
|
|
Expects(_images != nullptr);
|
|
|
|
if (largeEnabled) {
|
|
_images->ensureLoaded();
|
|
}
|
|
}
|
|
|
|
QImage EmojiImageLoader::prepare(EmojiPtr emoji) {
|
|
_images->ensureLoaded();
|
|
const auto factor = cIntRetinaFactor();
|
|
const auto side = st::largeEmojiSize + 2 * st::largeEmojiOutline;
|
|
auto tinted = QImage(
|
|
QSize(st::largeEmojiSize, st::largeEmojiSize) * factor,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
tinted.fill(Qt::white);
|
|
{
|
|
QPainter p(&tinted);
|
|
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
|
_images->draw(
|
|
p,
|
|
emoji,
|
|
st::largeEmojiSize * factor,
|
|
0,
|
|
0);
|
|
}
|
|
auto result = QImage(
|
|
QSize(side, side) * factor,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
result.fill(Qt::transparent);
|
|
{
|
|
QPainter p(&result);
|
|
const auto delta = st::largeEmojiOutline * factor;
|
|
const auto planar = std::array<QPoint, 4>{ {
|
|
{ 0, -1 },
|
|
{ -1, 0 },
|
|
{ 1, 0 },
|
|
{ 0, 1 },
|
|
} };
|
|
for (const auto &shift : planar) {
|
|
for (auto i = 0; i != delta; ++i) {
|
|
p.drawImage(QPoint(delta, delta) + shift * (i + 1), tinted);
|
|
}
|
|
}
|
|
const auto diagonal = std::array<QPoint, 4>{ {
|
|
{ -1, -1 },
|
|
{ 1, -1 },
|
|
{ -1, 1 },
|
|
{ 1, 1 },
|
|
} };
|
|
const auto corrected = int(std::round(delta / sqrt(2.)));
|
|
for (const auto &shift : diagonal) {
|
|
for (auto i = 0; i != corrected; ++i) {
|
|
p.drawImage(QPoint(delta, delta) + shift * (i + 1), tinted);
|
|
}
|
|
}
|
|
_images->draw(
|
|
p,
|
|
emoji,
|
|
st::largeEmojiSize * factor,
|
|
delta,
|
|
delta);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void EmojiImageLoader::switchTo(std::shared_ptr<UniversalImages> images) {
|
|
_images = std::move(images);
|
|
}
|
|
|
|
std::shared_ptr<UniversalImages> EmojiImageLoader::releaseImages() {
|
|
return std::exchange(
|
|
_images,
|
|
std::make_shared<UniversalImages>(_images->id()));
|
|
}
|
|
|
|
} // namespace details
|
|
|
|
EmojiPack::EmojiPack(not_null<Main::Session*> session)
|
|
: _session(session)
|
|
, _imageLoader(prepareSourceImages(), session->settings().largeEmoji())
|
|
, _clearTimer([=] { clearSourceImages(); }) {
|
|
refresh();
|
|
|
|
session->data().itemRemoved(
|
|
) | rpl::filter([](not_null<const HistoryItem*> item) {
|
|
return item->isIsolatedEmoji();
|
|
}) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
|
|
remove(item);
|
|
}, _lifetime);
|
|
|
|
_session->settings().largeEmojiChanges(
|
|
) | rpl::start_with_next([=](bool large) {
|
|
if (large) {
|
|
_clearTimer.cancel();
|
|
} else {
|
|
_clearTimer.callOnce(details::kClearSourceTimeout);
|
|
}
|
|
refreshAll();
|
|
}, _lifetime);
|
|
|
|
Ui::Emoji::Updated(
|
|
) | rpl::start_with_next([=] {
|
|
_images.clear();
|
|
_imageLoader.with([
|
|
source = prepareSourceImages()
|
|
](details::EmojiImageLoader &loader) mutable {
|
|
loader.switchTo(std::move(source));
|
|
});
|
|
refreshAll();
|
|
}, _lifetime);
|
|
}
|
|
|
|
EmojiPack::~EmojiPack() = default;
|
|
|
|
bool EmojiPack::add(not_null<HistoryItem*> item) {
|
|
auto length = 0;
|
|
if (const auto emoji = item->isolatedEmoji()) {
|
|
_items[emoji].emplace(item);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void EmojiPack::remove(not_null<const HistoryItem*> item) {
|
|
Expects(item->isIsolatedEmoji());
|
|
|
|
auto length = 0;
|
|
const auto emoji = item->isolatedEmoji();
|
|
const auto i = _items.find(emoji);
|
|
Assert(i != end(_items));
|
|
const auto j = i->second.find(item);
|
|
Assert(j != end(i->second));
|
|
i->second.erase(j);
|
|
if (i->second.empty()) {
|
|
_items.erase(i);
|
|
}
|
|
}
|
|
|
|
auto EmojiPack::stickerForEmoji(const IsolatedEmoji &emoji) -> Sticker {
|
|
Expects(!emoji.empty());
|
|
|
|
if (emoji.items[1] != nullptr) {
|
|
return Sticker();
|
|
}
|
|
const auto first = emoji.items[0];
|
|
const auto i = _map.find(first);
|
|
if (i != end(_map)) {
|
|
return { i->second.get(), nullptr };
|
|
}
|
|
if (!first->colored()) {
|
|
return Sticker();
|
|
}
|
|
const auto j = _map.find(first->original());
|
|
if (j != end(_map)) {
|
|
const auto index = first->variantIndex(first);
|
|
return { j->second.get(), details::ColorReplacements(index) };
|
|
}
|
|
return Sticker();
|
|
}
|
|
|
|
std::shared_ptr<Image> EmojiPack::image(EmojiPtr emoji) {
|
|
const auto i = _images.emplace(emoji, std::weak_ptr<Image>()).first;
|
|
if (const auto result = i->second.lock()) {
|
|
return result;
|
|
}
|
|
auto result = std::make_shared<Image>(
|
|
std::make_unique<details::ImageSource>(emoji, &_imageLoader));
|
|
i->second = result;
|
|
return result;
|
|
}
|
|
|
|
void EmojiPack::refresh() {
|
|
if (_requestId) {
|
|
return;
|
|
}
|
|
_requestId = _session->api().request(MTPmessages_GetStickerSet(
|
|
MTP_inputStickerSetAnimatedEmoji()
|
|
)).done([=](const MTPmessages_StickerSet &result) {
|
|
_requestId = 0;
|
|
refreshDelayed();
|
|
result.match([&](const MTPDmessages_stickerSet &data) {
|
|
applySet(data);
|
|
});
|
|
}).fail([=](const RPCError &error) {
|
|
_requestId = 0;
|
|
refreshDelayed();
|
|
}).send();
|
|
}
|
|
|
|
void EmojiPack::applySet(const MTPDmessages_stickerSet &data) {
|
|
const auto stickers = collectStickers(data.vdocuments().v);
|
|
auto was = base::take(_map);
|
|
|
|
for (const auto &pack : data.vpacks().v) {
|
|
pack.match([&](const MTPDstickerPack &data) {
|
|
applyPack(data, stickers);
|
|
});
|
|
}
|
|
|
|
for (const auto &[emoji, document] : _map) {
|
|
const auto i = was.find(emoji);
|
|
if (i == end(was)) {
|
|
refreshItems(emoji);
|
|
} else {
|
|
if (i->second != document) {
|
|
refreshItems(i->first);
|
|
}
|
|
was.erase(i);
|
|
}
|
|
}
|
|
for (const auto &[emoji, Document] : was) {
|
|
refreshItems(emoji);
|
|
}
|
|
}
|
|
|
|
void EmojiPack::refreshAll() {
|
|
for (const auto &[emoji, list] : _items) {
|
|
refreshItems(list);
|
|
}
|
|
}
|
|
|
|
void EmojiPack::refreshItems(EmojiPtr emoji) {
|
|
const auto i = _items.find(IsolatedEmoji{ { emoji } });
|
|
if (i == end(_items)) {
|
|
return;
|
|
}
|
|
refreshItems(i->second);
|
|
}
|
|
|
|
void EmojiPack::refreshItems(
|
|
const base::flat_set<not_null<HistoryItem*>> &list) {
|
|
for (const auto &item : list) {
|
|
_session->data().requestItemViewRefresh(item);
|
|
}
|
|
}
|
|
|
|
auto EmojiPack::prepareSourceImages()
|
|
-> std::shared_ptr<Ui::Emoji::UniversalImages> {
|
|
const auto &images = Ui::Emoji::SourceImages();
|
|
if (_session->settings().largeEmoji()) {
|
|
return images;
|
|
}
|
|
Ui::Emoji::ClearSourceImages(images);
|
|
return std::make_shared<Ui::Emoji::UniversalImages>(images->id());
|
|
}
|
|
|
|
void EmojiPack::clearSourceImages() {
|
|
_imageLoader.with([](details::EmojiImageLoader &loader) {
|
|
crl::on_main([images = loader.releaseImages()]{
|
|
Ui::Emoji::ClearSourceImages(images);
|
|
});
|
|
});
|
|
}
|
|
|
|
void EmojiPack::applyPack(
|
|
const MTPDstickerPack &data,
|
|
const base::flat_map<uint64, not_null<DocumentData*>> &map) {
|
|
const auto emoji = [&] {
|
|
return Ui::Emoji::Find(qs(data.vemoticon()));
|
|
}();
|
|
const auto document = [&]() -> DocumentData * {
|
|
for (const auto &id : data.vdocuments().v) {
|
|
const auto i = map.find(id.v);
|
|
if (i != end(map)) {
|
|
return i->second.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}();
|
|
if (emoji && document) {
|
|
_map.emplace_or_assign(emoji, document);
|
|
}
|
|
}
|
|
|
|
base::flat_map<uint64, not_null<DocumentData*>> EmojiPack::collectStickers(
|
|
const QVector<MTPDocument> &list) const {
|
|
auto result = base::flat_map<uint64, not_null<DocumentData*>>();
|
|
for (const auto &sticker : list) {
|
|
const auto document = _session->data().processDocument(
|
|
sticker);
|
|
if (document->sticker()) {
|
|
result.emplace(document->id, document);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void EmojiPack::refreshDelayed() {
|
|
App::CallDelayed(details::kRefreshTimeout, _session, [=] {
|
|
refresh();
|
|
});
|
|
}
|
|
|
|
} // namespace Stickers
|