diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp index 03231e8654..f32f89854f 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp @@ -9,57 +9,332 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.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 "base/concurrent_timer.h" #include "apiwrap.h" +#include "styles/style_history.h" namespace Stickers { +namespace details { + +class EmojiImageLoader { +public: + EmojiImageLoader( + crl::weak_on_queue weak, + int id); + + [[nodiscard]] QImage prepare(const IsolatedEmoji &emoji); + void switchTo(int id); + +private: + crl::weak_on_queue _weak; + std::optional _images; + + base::ConcurrentTimer _unloadTimer; + +}; + namespace { constexpr auto kRefreshTimeout = TimeId(7200); +constexpr auto kUnloadTimeout = 5 * crl::time(1000); + +[[nodiscard]] QSize CalculateSize(const IsolatedEmoji &emoji) { + using namespace rpl::mappers; + + const auto single = st::largeEmojiSize; + const auto skip = st::largeEmojiSkip; + const auto outline = st::largeEmojiOutline; + const auto count = ranges::count_if(emoji.items, _1 != nullptr); + const auto items = single * count + skip * (count - 1); + return QSize( + 2 * outline + items, + 2 * outline + single + ) * cIntRetinaFactor(); +} + +class ImageSource : public Images::Source { +public: + explicit ImageSource( + const IsolatedEmoji &emoji, + not_null*> 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 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*> _loader; + IsolatedEmoji _emoji; + QImage _data; + QByteArray _format; + QByteArray _bytes; + QSize _size; + base::binary_guard _loading; + +}; + +ImageSource::ImageSource( + const IsolatedEmoji &emoji, + not_null*> loader) +: _loader(loader) +, _emoji(emoji) +, _size(CalculateSize(emoji)) { +} + +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 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 -EmojiPack::EmojiPack(not_null session) : _session(session) { +EmojiImageLoader::EmojiImageLoader( + crl::weak_on_queue weak, + int id) +: _weak(std::move(weak)) +, _images(std::in_place, id) +, _unloadTimer(_weak.runner(), [=] { _images->clear(); }) { +} + +QImage EmojiImageLoader::prepare(const IsolatedEmoji &emoji) { + Expects(_images.has_value()); + + _images->ensureLoaded(); + auto result = QImage( + CalculateSize(emoji), + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + { + QPainter p(&result); + auto x = st::largeEmojiOutline; + const auto y = st::largeEmojiOutline; + for (const auto &single : emoji.items) { + if (!single) { + break; + } + _images->draw( + p, + single, + st::largeEmojiSize * cIntRetinaFactor(), + x, + y); + x += st::largeEmojiSize + st::largeEmojiSkip; + } + } + _unloadTimer.callOnce(kUnloadTimeout); + return result; +} + +void EmojiImageLoader::switchTo(int id) { + _images.emplace(id); +} + +} // namespace details + +EmojiPack::EmojiPack(not_null session) +: _session(session) +, _imageLoader(Ui::Emoji::CurrentSetId()) { refresh(); session->data().itemRemoved( ) | rpl::filter([](not_null item) { - return item->isSingleEmoji(); + return item->isIsolatedEmoji(); }) | rpl::start_with_next([=](not_null item) { remove(item); }, _lifetime); session->settings().largeEmojiChanges( ) | rpl::start_with_next([=] { - for (const auto &[emoji, document] : _map) { - refreshItems(emoji); - } + refreshAll(); + }, _lifetime); + + Ui::Emoji::Updated( + ) | rpl::start_with_next([=] { + const auto id = Ui::Emoji::CurrentSetId(); + _images.clear(); + _imageLoader.with([=](details::EmojiImageLoader &loader) { + loader.switchTo(id); + }); + refreshAll(); }, _lifetime); } -bool EmojiPack::add(not_null item, const QString &text) { +EmojiPack::~EmojiPack() = default; + +bool EmojiPack::add(not_null item) { auto length = 0; - const auto trimmed = text.trimmed(); - if (const auto emoji = Ui::Emoji::Find(trimmed, &length)) { - if (length == trimmed.size()) { - _items[emoji].emplace(item); - return true; - } + if (const auto emoji = item->isolatedEmoji()) { + _items[emoji].emplace(item); + return true; } return false; } -bool EmojiPack::remove(not_null item) { - if (!item->isSingleEmoji()) { - return false; - } +void EmojiPack::remove(not_null item) { + Expects(item->isIsolatedEmoji()); + auto length = 0; - const auto trimmed = item->originalString().trimmed(); - const auto emoji = Ui::Emoji::Find(trimmed, &length); - Assert(emoji != nullptr); - Assert(length == trimmed.size()); + const auto emoji = item->isolatedEmoji(); const auto i = _items.find(emoji); Assert(i != end(_items)); const auto j = i->second.find(item); @@ -68,22 +343,29 @@ bool EmojiPack::remove(not_null item) { if (i->second.empty()) { _items.erase(i); } - return true; } -DocumentData *EmojiPack::stickerForEmoji(not_null item) { - if (!item->isSingleEmoji() || !_session->settings().largeEmoji()) { +DocumentData *EmojiPack::stickerForEmoji(const IsolatedEmoji &emoji) { + Expects(!emoji.empty()); + + if (emoji.items[1] != nullptr) { return nullptr; } - auto length = 0; - const auto trimmed = item->originalString().trimmed(); - const auto emoji = Ui::Emoji::Find(trimmed, &length); - Assert(emoji != nullptr); - Assert(length == trimmed.size()); - const auto i = _map.find(emoji); + const auto i = _map.find(emoji.items[0]); return (i != end(_map)) ? i->second.get() : nullptr; } +std::shared_ptr EmojiPack::image(const IsolatedEmoji &emoji) { + const auto i = _images.emplace(emoji, std::weak_ptr()).first; + if (const auto result = i->second.lock()) { + return result; + } + auto result = std::make_shared( + std::make_unique(emoji, &_imageLoader)); + i->second = result; + return result; +} + void EmojiPack::refresh() { if (_requestId) { return; @@ -128,12 +410,23 @@ void EmojiPack::applySet(const MTPDmessages_stickerSet &data) { } } +void EmojiPack::refreshAll() { + for (const auto &[emoji, list] : _items) { + refreshItems(list); + } +} + void EmojiPack::refreshItems(EmojiPtr emoji) { - const auto i = _items.find(emoji); + const auto i = _items.find(IsolatedEmoji{ { emoji } }); if (i == end(_items)) { return; } - for (const auto &item : i->second) { + refreshItems(i->second); +} + +void EmojiPack::refreshItems( + const base::flat_set> &list) { + for (const auto &item : list) { _session->data().requestItemViewRefresh(item); } } @@ -172,7 +465,7 @@ base::flat_map> EmojiPack::collectStickers( } void EmojiPack::refreshDelayed() { - App::CallDelayed(kRefreshTimeout, _session, [=] { + App::CallDelayed(details::kRefreshTimeout, _session, [=] { refresh(); }); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h index 4d5efd1305..9295e3b51d 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h @@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/text/text_isolated_emoji.h" + +#include + namespace Main { class Session; } // namespace Main @@ -14,18 +18,33 @@ class Session; class HistoryItem; class DocumentData; +namespace Ui { +namespace Text { +class String; +} // namespace Text +} // namespace Ui + namespace Stickers { +namespace details { +class EmojiImageLoader; +} // namespace details + +using IsolatedEmoji = Ui::Text::IsolatedEmoji; class EmojiPack final { public: explicit EmojiPack(not_null session); + ~EmojiPack(); - bool add(not_null item, const QString &text); - bool remove(not_null item); + bool add(not_null item); + void remove(not_null item); - [[nodiscard]] DocumentData *stickerForEmoji(not_null item); + [[nodiscard]] DocumentData *stickerForEmoji(const IsolatedEmoji &emoji); + [[nodiscard]] std::shared_ptr image(const IsolatedEmoji &emoji); private: + class ImageLoader; + void refresh(); void refreshDelayed(); void applySet(const MTPDmessages_stickerSet &data); @@ -34,14 +53,20 @@ private: const base::flat_map> &map); base::flat_map> collectStickers( const QVector &list) const; + void refreshAll(); void refreshItems(EmojiPtr emoji); + void refreshItems(const base::flat_set> &list); not_null _session; - base::flat_set> _notLoaded; base::flat_map> _map; - base::flat_map>> _items; + base::flat_map< + IsolatedEmoji, + base::flat_set>> _items; + base::flat_map> _images; mtpRequestId _requestId = 0; + crl::object_on_queue _imageLoader; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 7b546c7e3a..7e49fdf9d3 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -771,9 +771,7 @@ std::unique_ptr MediaFile::createView( if (_document->sticker()) { return std::make_unique( message, - std::make_unique( - message, - _document)); + std::make_unique(message, _document)); } else if (_document->isAnimation()) { return std::make_unique(message, _document); } else if (_document->isVideoFile()) { diff --git a/Telegram/SourceFiles/history/history.style b/Telegram/SourceFiles/history/history.style index a9f3472094..9ef27d4972 100644 --- a/Telegram/SourceFiles/history/history.style +++ b/Telegram/SourceFiles/history/history.style @@ -582,3 +582,8 @@ historyAudioOutDownload: icon {{ "history_audio_download", historyFileOutIconFg historyAudioOutDownloadSelected: icon {{ "history_audio_download", historyFileOutIconFgSelected }}; historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px); + +largeEmojiSize: 36px; +largeEmojiOutline: 1px; +largeEmojiPadding: margins(0px, 0px, 0px, 20px); +largeEmojiSkip: 4px; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 78927694bf..34803b3eb7 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1681,7 +1681,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto media = (view ? view->media() : nullptr); const auto mediaHasTextForCopy = media && media->hasTextForCopy(); if (const auto document = media ? media->getDocument() : nullptr) { - if (!item->isSingleEmoji() && document->sticker()) { + if (!item->isIsolatedEmoji() && document->sticker()) { if (document->sticker()->set.type() != mtpc_inputStickerSetEmpty) { _menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] { showStickerPackInfo(document); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 417d896bf1..bb065f85ff 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "media/clip/media_clip_reader.h" #include "ui/effects/ripple_animation.h" +#include "ui/text/text_isolated_emoji.h" #include "ui/text_options.h" #include "storage/file_upload.h" #include "storage/storage_facade.h" @@ -779,6 +780,10 @@ QString HistoryItem::inDialogsText(DrawInDialog way) const { return plainText; } +Ui::Text::IsolatedEmoji HistoryItem::isolatedEmoji() const { + return Ui::Text::IsolatedEmoji(); +} + void HistoryItem::drawInDialog( Painter &p, const QRect &r, diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 7b6dbe5e1d..8c91de1d94 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -163,8 +163,8 @@ public: [[nodiscard]] bool isGroupMigrate() const { return isGroupEssential() && isEmpty(); } - [[nodiscard]] bool isSingleEmoji() const { - return _flags & MTPDmessage_ClientFlag::f_single_emoji; + [[nodiscard]] bool isIsolatedEmoji() const { + return _flags & MTPDmessage_ClientFlag::f_isolated_emoji; } [[nodiscard]] bool hasViews() const { return _flags & MTPDmessage::Flag::f_views; @@ -226,9 +226,7 @@ public: virtual QString inReplyText() const { return inDialogsText(DrawInDialog::WithoutSender); } - virtual QString originalString() const { - return QString(); - } + virtual Ui::Text::IsolatedEmoji isolatedEmoji() const; virtual TextWithEntities originalText() const { return TextWithEntities(); } diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index bf103bf11e..62420ec437 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_box.h" #include "ui/toast/toast.h" #include "ui/text/text_utilities.h" +#include "ui/text/text_isolated_emoji.h" #include "ui/text_options.h" #include "core/application.h" #include "layout.h" @@ -1084,7 +1085,7 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) { } } - clearSingleEmoji(); + clearIsolatedEmoji(); if (_media && _media->consumeMessageText(textWithEntities)) { setEmptyText(); } else { @@ -1100,7 +1101,7 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) { { QString::fromUtf8(":-("), EntitiesInText() }, Ui::ItemTextOptions(this)); } else if (!_media) { - checkSingleEmoji(textWithEntities.text); + checkIsolatedEmoji(); } _textWidth = -1; _textHeight = 0; @@ -1117,17 +1118,17 @@ void HistoryMessage::setEmptyText() { _textHeight = 0; } -void HistoryMessage::clearSingleEmoji() { - if (!(_flags & MTPDmessage_ClientFlag::f_single_emoji)) { +void HistoryMessage::clearIsolatedEmoji() { + if (!(_flags & MTPDmessage_ClientFlag::f_isolated_emoji)) { return; } history()->session().emojiStickersPack().remove(this); - _flags &= ~MTPDmessage_ClientFlag::f_single_emoji; + _flags &= ~MTPDmessage_ClientFlag::f_isolated_emoji; } -void HistoryMessage::checkSingleEmoji(const QString &text) { - if (history()->session().emojiStickersPack().add(this, text)) { - _flags |= MTPDmessage_ClientFlag::f_single_emoji; +void HistoryMessage::checkIsolatedEmoji() { + if (history()->session().emojiStickersPack().add(this)) { + _flags |= MTPDmessage_ClientFlag::f_isolated_emoji; } } @@ -1173,8 +1174,8 @@ void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) { } } -QString HistoryMessage::originalString() const { - return emptyText() ? QString() : _text.toString(); +Ui::Text::IsolatedEmoji HistoryMessage::isolatedEmoji() const { + return _text.toIsolatedEmoji(); } TextWithEntities HistoryMessage::originalText() const { diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index f692baad5b..a0c2568529 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -133,7 +133,7 @@ public: [[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const override; void setText(const TextWithEntities &textWithEntities) override; - [[nodiscard]] QString originalString() const override; + [[nodiscard]] Ui::Text::IsolatedEmoji isolatedEmoji() const override; [[nodiscard]] TextWithEntities originalText() const override; [[nodiscard]] TextForMimeData clipboardText() const override; [[nodiscard]] bool textHasLinks() const override; @@ -164,8 +164,8 @@ private: return _flags & MTPDmessage::Flag::f_legacy; } - void clearSingleEmoji(); - void checkSingleEmoji(const QString &text); + void clearIsolatedEmoji(); + void checkIsolatedEmoji(); // For an invoice button we replace the button text with a "Receipt" key. // It should show the receipt for the payed invoice. Still let mobile apps do that. diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 57ae48d5c5..5bc8eb13fa 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_sticker.h" +#include "history/view/media/history_view_large_emoji.h" #include "history/history.h" #include "main/main_session.h" #include "chat_helpers/stickers_emoji_pack.h" @@ -341,13 +342,22 @@ void Element::refreshMedia() { return; } } - const auto emojiStickers = &history()->session().emojiStickersPack(); - if (_data->media()) { - _media = _data->media()->createView(this); - } else if (const auto document = emojiStickers->stickerForEmoji(_data)) { - _media = std::make_unique( - this, - std::make_unique(this, document)); + const auto session = &history()->session(); + if (const auto media = _data->media()) { + _media = media->createView(this); + } else if (_data->isIsolatedEmoji() + && session->settings().largeEmoji()) { + const auto emoji = _data->isolatedEmoji(); + const auto emojiStickers = &session->emojiStickersPack(); + if (const auto document = emojiStickers->stickerForEmoji(emoji)) { + _media = std::make_unique( + this, + std::make_unique(this, document)); + } else { + _media = std::make_unique( + this, + std::make_unique(this, emoji)); + } } else { _media = nullptr; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_large_emoji.cpp b/Telegram/SourceFiles/history/view/media/history_view_large_emoji.cpp new file mode 100644 index 0000000000..449fbdbd80 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_large_emoji.cpp @@ -0,0 +1,68 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/media/history_view_large_emoji.h" + +#include "main/main_session.h" +#include "chat_helpers/stickers_emoji_pack.h" +#include "history/view/history_view_element.h" +#include "history/history_item.h" +#include "history/history.h" +#include "ui/image/image.h" +#include "data/data_file_origin.h" +#include "layout.h" +#include "styles/style_history.h" + +namespace HistoryView { +namespace { + +std::shared_ptr ResolveImage( + not_null session, + const Ui::Text::IsolatedEmoji &emoji) { + return session->emojiStickersPack().image(emoji); +} + +} // namespace + +LargeEmoji::LargeEmoji( + not_null parent, + Ui::Text::IsolatedEmoji emoji) +: _parent(parent) +, _emoji(emoji) +, _image(ResolveImage(&parent->data()->history()->session(), emoji)) { +} + +QSize LargeEmoji::size() { + const auto size = _image->size() / cIntRetinaFactor(); + const auto &padding = st::largeEmojiPadding; + _size = QSize( + padding.left() + size.width() + padding.right(), + padding.top() + size.height() + padding.bottom()); + return _size; +} + +void LargeEmoji::draw(Painter &p, const QRect &r, bool selected) { + _image->load(Data::FileOrigin()); + if (!_image->loaded()) { + return; + } + const auto &padding = st::largeEmojiPadding; + const auto o = Data::FileOrigin(); + const auto w = _size.width() - padding.left() - padding.right(); + const auto h = _size.height() - padding.top() - padding.bottom(); + const auto &c = st::msgStickerOverlay; + const auto pixmap = selected + ? _image->pixColored(o, c, w, h) + : _image->pix(o, w, h); + p.drawPixmap( + QPoint( + r.x() + (r.width() - _size.width()) / 2, + r.y() + (r.height() - _size.height()) / 2), + pixmap); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_large_emoji.h b/Telegram/SourceFiles/history/view/media/history_view_large_emoji.h new file mode 100644 index 0000000000..367e282c00 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_large_emoji.h @@ -0,0 +1,40 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "history/view/media/history_view_media_unwrapped.h" +#include "ui/text/text_isolated_emoji.h" + +namespace Data { +struct FileOrigin; +} // namespace Data + +namespace Lottie { +class SinglePlayer; +} // namespace Lottie + +namespace HistoryView { + +class LargeEmoji final : public UnwrappedMedia::Content { +public: + LargeEmoji( + not_null parent, + Ui::Text::IsolatedEmoji emoji); + + QSize size() override; + void draw(Painter &p, const QRect &r, bool selected) override; + +private: + const not_null _parent; + const Ui::Text::IsolatedEmoji _emoji; + std::shared_ptr _image; + QSize _size; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp index f21d576aad..cbf3673906 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp @@ -72,7 +72,7 @@ std::unique_ptr CreateAttach( if (document->sticker()) { return std::make_unique( parent, - std::make_unique(parent, document)); + std::make_unique(parent, document)); } else if (document->isAnimation()) { return std::make_unique(parent, document); } else if (document->isVideoFile()) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index e6bfe61f13..60b353f370 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -31,8 +31,9 @@ QSize UnwrappedMedia::countOptimalSize() { _contentSize = NonEmptySize(DownscaledSize( _content->size(), { st::maxStickerSize, st::maxStickerSize })); + const auto minimal = st::largeEmojiSize; auto maxWidth = std::max(_contentSize.width(), st::minPhotoSize); - auto minHeight = std::max(_contentSize.height(), st::minPhotoSize); + auto minHeight = std::max(_contentSize.height(), minimal); accumulate_max( maxWidth, _parent->infoWidth() + 2 * st::msgDateImgPadding.x()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h index 8b0cd24d03..00b77454f5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h @@ -22,8 +22,12 @@ public: class Content { public: [[nodiscard]] virtual QSize size() = 0; + virtual void draw(Painter &p, const QRect &r, bool selected) = 0; - [[nodiscard]] virtual ClickHandlerPtr link() = 0; + + [[nodiscard]] virtual ClickHandlerPtr link() { + return nullptr; + } [[nodiscard]] virtual DocumentData *document() { return nullptr; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index 80ad17ceb2..4c7d54d20d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -34,7 +34,7 @@ double GetEmojiStickerZoom(not_null session) { } // namespace -StickerContent::StickerContent( +Sticker::Sticker( not_null parent, not_null document) : _parent(parent) @@ -42,15 +42,15 @@ StickerContent::StickerContent( _document->loadThumbnail(parent->data()->fullId()); } -StickerContent::~StickerContent() { +Sticker::~Sticker() { unloadLottie(); } -bool StickerContent::isEmojiSticker() const { +bool Sticker::isEmojiSticker() const { return (_parent->data()->media() == nullptr); } -QSize StickerContent::size() { +QSize Sticker::size() { _size = _document->dimensions; if (isEmojiSticker()) { constexpr auto kIdealStickerSize = 512; @@ -63,10 +63,7 @@ QSize StickerContent::size() { return _size; } -void StickerContent::draw( - Painter &p, - const QRect &r, - bool selected) { +void Sticker::draw(Painter &p, const QRect &r, bool selected) { const auto sticker = _document->sticker(); if (!sticker) { return; @@ -85,7 +82,7 @@ void StickerContent::draw( } } -void StickerContent::paintLottie(Painter &p, const QRect &r, bool selected) { +void Sticker::paintLottie(Painter &p, const QRect &r, bool selected) { auto request = Lottie::FrameRequest(); request.box = _size * cIntRetinaFactor(); if (selected) { @@ -114,7 +111,7 @@ void StickerContent::paintLottie(Painter &p, const QRect &r, bool selected) { } } -void StickerContent::paintPixmap(Painter &p, const QRect &r, bool selected) { +void Sticker::paintPixmap(Painter &p, const QRect &r, bool selected) { const auto pixmap = paintedPixmap(selected); if (!pixmap.isNull()) { p.drawPixmap( @@ -125,7 +122,7 @@ void StickerContent::paintPixmap(Painter &p, const QRect &r, bool selected) { } } -QPixmap StickerContent::paintedPixmap(bool selected) const { +QPixmap Sticker::paintedPixmap(bool selected) const { const auto o = _parent->data()->fullId(); const auto w = _size.width(); const auto h = _size.height(); @@ -157,7 +154,7 @@ QPixmap StickerContent::paintedPixmap(bool selected) const { return QPixmap(); } -void StickerContent::refreshLink() { +void Sticker::refreshLink() { if (_link) { return; } @@ -180,7 +177,7 @@ void StickerContent::refreshLink() { } } -void StickerContent::setupLottie() { +void Sticker::setupLottie() { _lottie = Stickers::LottiePlayerFromDocument( _document, Stickers::LottieSize::MessageHistory, @@ -198,7 +195,7 @@ void StickerContent::setupLottie() { }, _lifetime); } -void StickerContent::unloadLottie() { +void Sticker::unloadLottie() { if (!_lottie) { return; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.h b/Telegram/SourceFiles/history/view/media/history_view_sticker.h index d2dddfd2bb..9b454364a0 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.h +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.h @@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_unwrapped.h" #include "base/weak_ptr.h" -#include "base/timer.h" namespace Data { struct FileOrigin; @@ -21,14 +20,14 @@ class SinglePlayer; namespace HistoryView { -class StickerContent final +class Sticker final : public UnwrappedMedia::Content , public base::has_weak_ptr { public: - StickerContent( + Sticker( not_null parent, not_null document); - ~StickerContent(); + ~Sticker(); QSize size() override; void draw(Painter &p, const QRect &r, bool selected) override; diff --git a/Telegram/SourceFiles/mtproto/type_utils.h b/Telegram/SourceFiles/mtproto/type_utils.h index 8d01f3f84d..bc76637c2b 100644 --- a/Telegram/SourceFiles/mtproto/type_utils.h +++ b/Telegram/SourceFiles/mtproto/type_utils.h @@ -63,8 +63,8 @@ enum class MTPDmessage_ClientFlag : uint32 { // message was an outgoing message and failed to be sent f_failed = (1U << 22), - // message has no media and only a single emoji text - f_single_emoji = (1U << 21), + // message has no media and only a several emoji text + f_isolated_emoji = (1U << 21), // update this when adding new client side flags MIN_FIELD = (1U << 21), diff --git a/Telegram/SourceFiles/ui/emoji_config.cpp b/Telegram/SourceFiles/ui/emoji_config.cpp index 2b926763f3..9f123ae784 100644 --- a/Telegram/SourceFiles/ui/emoji_config.cpp +++ b/Telegram/SourceFiles/ui/emoji_config.cpp @@ -61,26 +61,6 @@ private: }; -class UniversalImages { -public: - explicit UniversalImages(int id); - - int id() const; - bool ensureLoaded(); - void clear(); - - void draw(QPainter &p, EmojiPtr emoji, int size, int x, int y) const; - - // This method must be thread safe and so it is called after - // the _id value is fixed and all _sprites are loaded. - QImage generate(int size, int index) const; - -private: - int _id = 0; - std::vector _sprites; - -}; - auto SizeNormal = -1; auto SizeLarge = -1; auto SpritesCount = -1; @@ -359,6 +339,71 @@ std::vector LoadAndValidateSprites(int id) { return result; } +void AppendPartToResult(TextWithEntities &result, const QChar *start, const QChar *from, const QChar *to) { + if (to <= from) { + return; + } + for (auto &entity : result.entities) { + if (entity.offset() >= to - start) break; + if (entity.offset() + entity.length() < from - start) continue; + if (entity.offset() >= from - start) { + entity.extendToLeft(from - start - result.text.size()); + } + if (entity.offset() + entity.length() <= to - start) { + entity.shrinkFromRight(from - start - result.text.size()); + } + } + result.text.append(from, to - from); +} + +bool IsReplacementPart(ushort ch) { + return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '-') || (ch == '+') || (ch == '_'); +} + +EmojiPtr FindReplacement(const QChar *start, const QChar *end, int *outLength) { + if (start != end && *start == ':') { + auto maxLength = GetSuggestionMaxLength(); + for (auto till = start + 1; till != end; ++till) { + if (*till == ':') { + auto text = QString::fromRawData(start, till + 1 - start); + auto emoji = GetSuggestionEmoji(QStringToUTF16(text)); + auto result = Find(QStringFromUTF16(emoji)); + if (result) { + if (outLength) *outLength = (till + 1 - start); + } + return result; + } else if (!IsReplacementPart(till->unicode()) || (till - start) > maxLength) { + break; + } + } + } + return internal::FindReplace(start, end, outLength); +} + +void ClearUniversalChecked() { + Expects(InstanceNormal != nullptr && InstanceLarge != nullptr); + + if (InstanceNormal->cached() && InstanceLarge->cached() && Universal) { + Universal->clear(); + } +} + +} // namespace + +namespace internal { + +QString CacheFileFolder() { + return cWorkingDir() + "tdata/emoji"; +} + +QString SetDataPath(int id) { + Expects(IsValidSetId(id) && id != 0); + + return CacheFileFolder() + "/set" + QString::number(id); +} + +} // namespace internal + UniversalImages::UniversalImages(int id) : _id(id) { Expects(IsValidSetId(id)); } @@ -452,71 +497,6 @@ QImage UniversalImages::generate(int size, int index) const { return result; } -void AppendPartToResult(TextWithEntities &result, const QChar *start, const QChar *from, const QChar *to) { - if (to <= from) { - return; - } - for (auto &entity : result.entities) { - if (entity.offset() >= to - start) break; - if (entity.offset() + entity.length() < from - start) continue; - if (entity.offset() >= from - start) { - entity.extendToLeft(from - start - result.text.size()); - } - if (entity.offset() + entity.length() <= to - start) { - entity.shrinkFromRight(from - start - result.text.size()); - } - } - result.text.append(from, to - from); -} - -bool IsReplacementPart(ushort ch) { - return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '-') || (ch == '+') || (ch == '_'); -} - -EmojiPtr FindReplacement(const QChar *start, const QChar *end, int *outLength) { - if (start != end && *start == ':') { - auto maxLength = GetSuggestionMaxLength(); - for (auto till = start + 1; till != end; ++till) { - if (*till == ':') { - auto text = QString::fromRawData(start, till + 1 - start); - auto emoji = GetSuggestionEmoji(QStringToUTF16(text)); - auto result = Find(QStringFromUTF16(emoji)); - if (result) { - if (outLength) *outLength = (till + 1 - start); - } - return result; - } else if (!IsReplacementPart(till->unicode()) || (till - start) > maxLength) { - break; - } - } - } - return internal::FindReplace(start, end, outLength); -} - -void ClearUniversalChecked() { - Expects(InstanceNormal != nullptr && InstanceLarge != nullptr); - - if (InstanceNormal->cached() && InstanceLarge->cached() && Universal) { - Universal->clear(); - } -} - -} // namespace - -namespace internal { - -QString CacheFileFolder() { - return cWorkingDir() + "tdata/emoji"; -} - -QString SetDataPath(int id) { - Expects(IsValidSetId(id) && id != 0); - - return CacheFileFolder() + "/set" + QString::number(id); -} - -} // namespace internal - void Init() { internal::Init(); diff --git a/Telegram/SourceFiles/ui/emoji_config.h b/Telegram/SourceFiles/ui/emoji_config.h index 522f854bc4..fb056e6a91 100644 --- a/Telegram/SourceFiles/ui/emoji_config.h +++ b/Telegram/SourceFiles/ui/emoji_config.h @@ -167,5 +167,25 @@ rpl::producer<> UpdatedRecent(); const QPixmap &SinglePixmap(EmojiPtr emoji, int fontHeight); void Draw(QPainter &p, EmojiPtr emoji, int size, int x, int y); +class UniversalImages { +public: + explicit UniversalImages(int id); + + int id() const; + bool ensureLoaded(); + void clear(); + + void draw(QPainter &p, EmojiPtr emoji, int size, int x, int y) const; + + // This method must be thread safe and so it is called after + // the _id value is fixed and all _sprites are loaded. + QImage generate(int size, int index) const; + +private: + int _id = 0; + std::vector _sprites; + +}; + } // namespace Emoji } // namespace Ui diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index 94997c0c0b..99d27b60c8 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "core/crash_reports.h" #include "ui/text/text_block.h" +#include "ui/text/text_isolated_emoji.h" #include "ui/emoji_config.h" #include "lang/lang_keys.h" #include "platform/platform_info.h" @@ -3311,6 +3312,25 @@ TextForMimeData String::toText( return result; } +IsolatedEmoji String::toIsolatedEmoji() const { + auto result = IsolatedEmoji(); + const auto skip = (_blocks.empty() + || _blocks.back()->type() != TextBlockTSkip) ? 0 : 1; + if (_blocks.size() > kIsolatedEmojiLimit + skip) { + return IsolatedEmoji(); + } + auto index = 0; + for (const auto &block : _blocks) { + const auto type = block->type(); + if (type == TextBlockTEmoji) { + result.items[index++] = static_cast(block.get())->emoji; + } else if (type != TextBlockTSkip) { + return IsolatedEmoji(); + } + } + return result; +} + void String::clear() { clearFields(); _text.clear(); diff --git a/Telegram/SourceFiles/ui/text/text.h b/Telegram/SourceFiles/ui/text/text.h index 86b265a736..537c0b60df 100644 --- a/Telegram/SourceFiles/ui/text/text.h +++ b/Telegram/SourceFiles/ui/text/text.h @@ -7,8 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "core/click_handler.h" #include "ui/text/text_entity.h" +#include "core/click_handler.h" #include "base/flags.h" #include @@ -70,6 +70,7 @@ namespace Ui { namespace Text { class AbstractBlock; +struct IsolatedEmoji; struct StateRequest { enum class Flag { @@ -176,6 +177,7 @@ public: TextSelection selection = AllTextSelection) const; TextForMimeData toTextForMimeData( TextSelection selection = AllTextSelection) const; + IsolatedEmoji toIsolatedEmoji() const; bool lastDots(int32 dots, int32 maxdots = 3) { // hack for typing animation if (_text.size() < maxdots) return false; diff --git a/Telegram/SourceFiles/ui/text/text_isolated_emoji.h b/Telegram/SourceFiles/ui/text/text_isolated_emoji.h new file mode 100644 index 0000000000..42bb38339d --- /dev/null +++ b/Telegram/SourceFiles/ui/text/text_isolated_emoji.h @@ -0,0 +1,46 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Ui { +namespace Text { + +inline constexpr auto kIsolatedEmojiLimit = 3; + +struct IsolatedEmoji { + using Items = std::array; + Items items = { { nullptr } }; + + [[nodiscard]] bool empty() const { + return items[0] == nullptr; + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } + [[nodiscard]] bool operator<(const IsolatedEmoji &other) const { + return items < other.items; + } + [[nodiscard]] bool operator==(const IsolatedEmoji &other) const { + return items == other.items; + } + [[nodiscard]] bool operator>(const IsolatedEmoji &other) const { + return other < *this; + } + [[nodiscard]] bool operator<=(const IsolatedEmoji &other) const { + return !(other < *this); + } + [[nodiscard]] bool operator>=(const IsolatedEmoji &other) const { + return !(*this < other); + } + [[nodiscard]] bool operator!=(const IsolatedEmoji &other) const { + return !(*this == other); + } +}; + +} // namespace Text +} // namespace Ui diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index 444b191c46..02e4e2ffde 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -288,6 +288,8 @@ <(src_loc)/history/view/media/history_view_gif.cpp <(src_loc)/history/view/media/history_view_invoice.h <(src_loc)/history/view/media/history_view_invoice.cpp +<(src_loc)/history/view/media/history_view_large_emoji.h +<(src_loc)/history/view/media/history_view_large_emoji.cpp <(src_loc)/history/view/media/history_view_location.h <(src_loc)/history/view/media/history_view_location.cpp <(src_loc)/history/view/media/history_view_media.h @@ -760,6 +762,7 @@ <(src_loc)/ui/text/text_block.h <(src_loc)/ui/text/text_entity.cpp <(src_loc)/ui/text/text_entity.h +<(src_loc)/ui/text/text_isolated_emoji.h <(src_loc)/ui/text/text_utilities.cpp <(src_loc)/ui/text/text_utilities.h <(src_loc)/ui/toast/toast.cpp