533 lines
14 KiB
C++
533 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 "chat_helpers/stickers_emoji_image_loader.h"
|
|
#include "history/view/history_view_element.h"
|
|
#include "history/history_item.h"
|
|
#include "history/history.h"
|
|
#include "lottie/lottie_common.h"
|
|
#include "ui/emoji_config.h"
|
|
#include "ui/text/text_isolated_emoji.h"
|
|
#include "ui/image/image.h"
|
|
#include "ui/rect.h"
|
|
#include "main/main_session.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_document.h"
|
|
#include "data/stickers/data_custom_emoji.h"
|
|
#include "core/core_settings.h"
|
|
#include "core/application.h"
|
|
#include "base/call_delayed.h"
|
|
#include "chat_helpers/stickers_lottie.h"
|
|
#include "history/view/media/history_view_sticker.h"
|
|
#include "lottie/lottie_single_player.h"
|
|
#include "apiwrap.h"
|
|
#include "styles/style_chat.h"
|
|
|
|
#include <QtCore/QBuffer>
|
|
|
|
namespace Stickers {
|
|
namespace {
|
|
|
|
constexpr auto kRefreshTimeout = 7200 * crl::time(1000);
|
|
constexpr auto kEmojiCachesCount = 4;
|
|
constexpr auto kPremiumCachesCount = 8;
|
|
|
|
[[nodiscard]] std::optional<int> IndexFromEmoticon(const QString &emoticon) {
|
|
if (emoticon.size() < 2) {
|
|
return std::nullopt;
|
|
}
|
|
const auto first = emoticon[0].unicode();
|
|
return (first >= '1' && first <= '9')
|
|
? std::make_optional(first - '1')
|
|
: (first == 55357 && emoticon[1].unicode() == 56607)
|
|
? std::make_optional(9)
|
|
: std::nullopt;
|
|
}
|
|
|
|
[[nodiscard]] QSize SingleSize() {
|
|
const auto single = st::largeEmojiSize;
|
|
const auto outline = st::largeEmojiOutline;
|
|
return Size(2 * outline + single) * style::DevicePixelRatio();
|
|
}
|
|
|
|
[[nodiscard]] const Lottie::ColorReplacements *ColorReplacements(int index) {
|
|
Expects(index >= 1 && index <= 5);
|
|
|
|
static const auto color1 = Lottie::ColorReplacements{
|
|
.modifier = Lottie::SkinModifier::Color1,
|
|
.tag = 1,
|
|
};
|
|
static const auto color2 = Lottie::ColorReplacements{
|
|
.modifier = Lottie::SkinModifier::Color2,
|
|
.tag = 2,
|
|
};
|
|
static const auto color3 = Lottie::ColorReplacements{
|
|
.modifier = Lottie::SkinModifier::Color3,
|
|
.tag = 3,
|
|
};
|
|
static const auto color4 = Lottie::ColorReplacements{
|
|
.modifier = Lottie::SkinModifier::Color4,
|
|
.tag = 4,
|
|
};
|
|
static const auto color5 = Lottie::ColorReplacements{
|
|
.modifier = Lottie::SkinModifier::Color5,
|
|
.tag = 5,
|
|
};
|
|
static const auto list = std::array{
|
|
&color1,
|
|
&color2,
|
|
&color3,
|
|
&color4,
|
|
&color5,
|
|
};
|
|
return list[index - 1];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
QSize LargeEmojiImage::Size() {
|
|
return SingleSize();
|
|
}
|
|
|
|
EmojiPack::EmojiPack(not_null<Main::Session*> session)
|
|
: _session(session) {
|
|
refresh();
|
|
|
|
session->data().viewRemoved(
|
|
) | rpl::filter([](not_null<const ViewElement*> view) {
|
|
return view->isIsolatedEmoji() || view->isOnlyCustomEmoji();
|
|
}) | rpl::start_with_next([=](not_null<const ViewElement*> item) {
|
|
remove(item);
|
|
}, _lifetime);
|
|
|
|
Core::App().settings().largeEmojiChanges(
|
|
) | rpl::start_with_next([=](bool large) {
|
|
refreshAll();
|
|
}, _lifetime);
|
|
|
|
Ui::Emoji::Updated(
|
|
) | rpl::start_with_next([=] {
|
|
_images.clear();
|
|
refreshAll();
|
|
}, _lifetime);
|
|
}
|
|
|
|
EmojiPack::~EmojiPack() = default;
|
|
|
|
bool EmojiPack::add(not_null<ViewElement*> view) {
|
|
if (const auto custom = view->onlyCustomEmoji()) {
|
|
_onlyCustomItems.emplace(view);
|
|
return true;
|
|
} else if (const auto emoji = view->isolatedEmoji()) {
|
|
_items[emoji].emplace(view);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void EmojiPack::remove(not_null<const ViewElement*> view) {
|
|
Expects(view->isIsolatedEmoji() || view->isOnlyCustomEmoji());
|
|
|
|
if (view->isOnlyCustomEmoji()) {
|
|
_onlyCustomItems.remove(view);
|
|
} else if (const auto emoji = view->isolatedEmoji()) {
|
|
const auto i = _items.find(emoji);
|
|
Assert(i != end(_items));
|
|
const auto j = i->second.find(view);
|
|
Assert(j != end(i->second));
|
|
i->second.erase(j);
|
|
if (i->second.empty()) {
|
|
_items.erase(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto EmojiPack::stickerForEmoji(EmojiPtr emoji) -> Sticker {
|
|
Expects(emoji != nullptr);
|
|
|
|
const auto i = _map.find(emoji);
|
|
if (i != end(_map)) {
|
|
return { i->second.get(), nullptr };
|
|
}
|
|
if (!emoji->colored()) {
|
|
return {};
|
|
}
|
|
const auto j = _map.find(emoji->original());
|
|
if (j != end(_map)) {
|
|
const auto index = emoji->variantIndex(emoji);
|
|
return { j->second.get(), ColorReplacements(index) };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
auto EmojiPack::stickerForEmoji(const IsolatedEmoji &emoji) -> Sticker {
|
|
Expects(!emoji.empty());
|
|
|
|
if (!v::is_null(emoji.items[1])) {
|
|
return {};
|
|
} else if (const auto regular = std::get_if<EmojiPtr>(&emoji.items[0])) {
|
|
return stickerForEmoji(*regular);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::shared_ptr<LargeEmojiImage> EmojiPack::image(EmojiPtr emoji) {
|
|
const auto i = _images.emplace(
|
|
emoji,
|
|
std::weak_ptr<LargeEmojiImage>()).first;
|
|
if (const auto result = i->second.lock()) {
|
|
return result;
|
|
}
|
|
auto result = std::make_shared<LargeEmojiImage>();
|
|
const auto raw = result.get();
|
|
const auto weak = base::make_weak(_session);
|
|
raw->load = [=] {
|
|
Core::App().emojiImageLoader().with([=](
|
|
const EmojiImageLoader &loader) {
|
|
crl::on_main(weak, [
|
|
=,
|
|
image = loader.prepare(emoji)
|
|
]() mutable {
|
|
const auto i = _images.find(emoji);
|
|
if (i != end(_images)) {
|
|
if (const auto strong = i->second.lock()) {
|
|
if (!strong->image) {
|
|
strong->load = nullptr;
|
|
strong->image.emplace(std::move(image));
|
|
_session->notifyDownloaderTaskFinished();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
raw->load = nullptr;
|
|
};
|
|
i->second = result;
|
|
return result;
|
|
}
|
|
|
|
EmojiPtr EmojiPack::chooseInteractionEmoji(
|
|
not_null<HistoryItem*> item) const {
|
|
return chooseInteractionEmoji(item->originalText().text);
|
|
}
|
|
|
|
EmojiPtr EmojiPack::chooseInteractionEmoji(
|
|
const QString &emoticon) const {
|
|
const auto emoji = Ui::Emoji::Find(emoticon);
|
|
if (!emoji) {
|
|
return nullptr;
|
|
}
|
|
if (!animationsForEmoji(emoji).empty()) {
|
|
return emoji;
|
|
}
|
|
if (const auto original = emoji->original(); original != emoji) {
|
|
if (!animationsForEmoji(original).empty()) {
|
|
return original;
|
|
}
|
|
}
|
|
static const auto kHearts = {
|
|
QString::fromUtf8("\xf0\x9f\x92\x9b"),
|
|
QString::fromUtf8("\xf0\x9f\x92\x99"),
|
|
QString::fromUtf8("\xf0\x9f\x92\x9a"),
|
|
QString::fromUtf8("\xf0\x9f\x92\x9c"),
|
|
QString::fromUtf8("\xf0\x9f\xa7\xa1"),
|
|
QString::fromUtf8("\xf0\x9f\x96\xa4"),
|
|
QString::fromUtf8("\xf0\x9f\xa4\x8e"),
|
|
QString::fromUtf8("\xf0\x9f\xa4\x8d"),
|
|
};
|
|
return ranges::contains(kHearts, emoji->id())
|
|
? Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\xa4"))
|
|
: emoji;
|
|
}
|
|
|
|
auto EmojiPack::animationsForEmoji(EmojiPtr emoji) const
|
|
-> const base::flat_map<int, not_null<DocumentData*>> & {
|
|
static const auto empty = base::flat_map<int, not_null<DocumentData*>>();
|
|
if (!emoji) {
|
|
return empty;
|
|
}
|
|
const auto i = _animations.find(emoji);
|
|
return (i != end(_animations)) ? i->second : empty;
|
|
}
|
|
|
|
bool EmojiPack::hasAnimationsFor(not_null<HistoryItem*> item) const {
|
|
return !animationsForEmoji(chooseInteractionEmoji(item)).empty();
|
|
}
|
|
|
|
bool EmojiPack::hasAnimationsFor(const QString &emoticon) const {
|
|
return !animationsForEmoji(chooseInteractionEmoji(emoticon)).empty();
|
|
}
|
|
|
|
std::unique_ptr<Lottie::SinglePlayer> EmojiPack::effectPlayer(
|
|
not_null<DocumentData*> document,
|
|
QByteArray data,
|
|
QString filepath,
|
|
EffectType type) {
|
|
// Shortened copy from stickers_lottie module.
|
|
const auto baseKey = document->bigFileBaseCacheKey();
|
|
const auto tag = uint8(type);
|
|
const auto keyShift = ((tag << 4) & 0xF0)
|
|
| (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F);
|
|
const auto key = Storage::Cache::Key{
|
|
baseKey.high,
|
|
baseKey.low + keyShift
|
|
};
|
|
const auto get = [=](int i, FnMut<void(QByteArray &&cached)> handler) {
|
|
document->owner().cacheBigFile().get(
|
|
{ key.high, key.low + i },
|
|
std::move(handler));
|
|
};
|
|
const auto weak = base::make_weak(&document->session());
|
|
const auto put = [=](int i, QByteArray &&cached) {
|
|
crl::on_main(weak, [=, data = std::move(cached)]() mutable {
|
|
weak->data().cacheBigFile().put(
|
|
{ key.high, key.low + i },
|
|
std::move(data));
|
|
});
|
|
};
|
|
const auto size = (type == EffectType::PremiumSticker)
|
|
? HistoryView::Sticker::PremiumEffectSize(document)
|
|
: (type == EffectType::EmojiInteraction)
|
|
? HistoryView::Sticker::EmojiEffectSize()
|
|
: HistoryView::Sticker::MessageEffectSize();
|
|
const auto request = Lottie::FrameRequest{
|
|
size * style::DevicePixelRatio(),
|
|
};
|
|
auto &weakProvider = _sharedProviders[{ document, type }];
|
|
auto shared = [&] {
|
|
if (const auto result = weakProvider.lock()) {
|
|
return result;
|
|
}
|
|
const auto count = (type == EffectType::PremiumSticker)
|
|
? kPremiumCachesCount
|
|
: kEmojiCachesCount;
|
|
const auto result = Lottie::SinglePlayer::SharedProvider(
|
|
count,
|
|
get,
|
|
put,
|
|
Lottie::ReadContent(data, filepath),
|
|
request,
|
|
Lottie::Quality::High);
|
|
weakProvider = result;
|
|
return result;
|
|
}();
|
|
return std::make_unique<Lottie::SinglePlayer>(std::move(shared), request);
|
|
}
|
|
|
|
void EmojiPack::refresh() {
|
|
if (_requestId) {
|
|
return;
|
|
}
|
|
_requestId = _session->api().request(MTPmessages_GetStickerSet(
|
|
MTP_inputStickerSetAnimatedEmoji(),
|
|
MTP_int(0) // hash
|
|
)).done([=](const MTPmessages_StickerSet &result) {
|
|
_requestId = 0;
|
|
refreshAnimations();
|
|
result.match([&](const MTPDmessages_stickerSet &data) {
|
|
applySet(data);
|
|
}, [](const MTPDmessages_stickerSetNotModified &) {
|
|
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
|
|
});
|
|
}).fail([=](const MTP::Error &error) {
|
|
_requestId = 0;
|
|
refreshDelayed();
|
|
}).send();
|
|
}
|
|
|
|
void EmojiPack::refreshAnimations() {
|
|
if (_animationsRequestId) {
|
|
return;
|
|
}
|
|
_animationsRequestId = _session->api().request(MTPmessages_GetStickerSet(
|
|
MTP_inputStickerSetAnimatedEmojiAnimations(),
|
|
MTP_int(0) // hash
|
|
)).done([=](const MTPmessages_StickerSet &result) {
|
|
_animationsRequestId = 0;
|
|
refreshDelayed();
|
|
result.match([&](const MTPDmessages_stickerSet &data) {
|
|
applyAnimationsSet(data);
|
|
}, [](const MTPDmessages_stickerSetNotModified &) {
|
|
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
|
|
});
|
|
}).fail([=] {
|
|
_animationsRequestId = 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);
|
|
}
|
|
_refreshed.fire({});
|
|
}
|
|
|
|
void EmojiPack::applyAnimationsSet(const MTPDmessages_stickerSet &data) {
|
|
const auto stickers = collectStickers(data.vdocuments().v);
|
|
const auto &packs = data.vpacks().v;
|
|
const auto indices = collectAnimationsIndices(packs);
|
|
|
|
_animations.clear();
|
|
for (const auto &pack : packs) {
|
|
pack.match([&](const MTPDstickerPack &data) {
|
|
const auto emoticon = qs(data.vemoticon());
|
|
if (IndexFromEmoticon(emoticon).has_value()) {
|
|
return;
|
|
}
|
|
const auto emoji = Ui::Emoji::Find(emoticon);
|
|
if (!emoji) {
|
|
return;
|
|
}
|
|
for (const auto &id : data.vdocuments().v) {
|
|
const auto i = indices.find(id.v);
|
|
if (i == end(indices)) {
|
|
continue;
|
|
}
|
|
const auto j = stickers.find(id.v);
|
|
if (j == end(stickers)) {
|
|
continue;
|
|
}
|
|
for (const auto index : i->second) {
|
|
_animations[emoji].emplace(index, j->second);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
++_animationsVersion;
|
|
}
|
|
|
|
auto EmojiPack::collectAnimationsIndices(
|
|
const QVector<MTPStickerPack> &packs
|
|
) const -> base::flat_map<uint64, base::flat_set<int>> {
|
|
auto result = base::flat_map<uint64, base::flat_set<int>>();
|
|
for (const auto &pack : packs) {
|
|
pack.match([&](const MTPDstickerPack &data) {
|
|
if (const auto index = IndexFromEmoticon(qs(data.vemoticon()))) {
|
|
for (const auto &id : data.vdocuments().v) {
|
|
result[id.v].emplace(*index);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void EmojiPack::refreshAll() {
|
|
auto items = base::flat_set<not_null<HistoryItem*>>();
|
|
auto count = 0;
|
|
for (const auto &[emoji, list] : _items) {
|
|
// refreshItems(list); // This call changes _items!
|
|
count += int(list.size());
|
|
}
|
|
items.reserve(count);
|
|
for (const auto &[emoji, list] : _items) {
|
|
// refreshItems(list); // This call changes _items!
|
|
for (const auto &view : list) {
|
|
items.emplace(view->data());
|
|
}
|
|
}
|
|
refreshItems(items);
|
|
refreshItems(_onlyCustomItems);
|
|
}
|
|
|
|
void EmojiPack::refreshItems(EmojiPtr emoji) {
|
|
const auto i = _items.find(IsolatedEmoji{ { emoji } });
|
|
if (!emoji->colored()) {
|
|
if (const auto count = emoji->variantsCount()) {
|
|
for (auto i = 0; i != count; ++i) {
|
|
refreshItems(emoji->variant(i + 1));
|
|
}
|
|
}
|
|
}
|
|
if (i == end(_items)) {
|
|
return;
|
|
}
|
|
refreshItems(i->second);
|
|
}
|
|
|
|
void EmojiPack::refreshItems(
|
|
const base::flat_set<not_null<ViewElement*>> &list) {
|
|
auto items = base::flat_set<not_null<HistoryItem*>>();
|
|
items.reserve(list.size());
|
|
for (const auto &view : list) {
|
|
items.emplace(view->data());
|
|
}
|
|
refreshItems(items);
|
|
}
|
|
|
|
void EmojiPack::refreshItems(
|
|
const base::flat_set<not_null<HistoryItem*>> &items) {
|
|
for (const auto &item : items) {
|
|
_session->data().requestItemViewRefresh(item);
|
|
}
|
|
}
|
|
|
|
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() {
|
|
base::call_delayed(kRefreshTimeout, _session, [=] {
|
|
refresh();
|
|
});
|
|
}
|
|
|
|
} // namespace Stickers
|