diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp
index 03231e865..f32f89854 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<EmojiImageLoader> weak,
+		int id);
+
+	[[nodiscard]] QImage prepare(const IsolatedEmoji &emoji);
+	void switchTo(int id);
+
+private:
+	crl::weak_on_queue<EmojiImageLoader> _weak;
+	std::optional<Ui::Emoji::UniversalImages> _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<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;
+	IsolatedEmoji _emoji;
+	QImage _data;
+	QByteArray _format;
+	QByteArray _bytes;
+	QSize _size;
+	base::binary_guard _loading;
+
+};
+
+ImageSource::ImageSource(
+	const IsolatedEmoji &emoji,
+	not_null<crl::object_on_queue<EmojiImageLoader>*> 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<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
 
-EmojiPack::EmojiPack(not_null<Main::Session*> session) : _session(session) {
+EmojiImageLoader::EmojiImageLoader(
+	crl::weak_on_queue<EmojiImageLoader> 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<Main::Session*> session)
+: _session(session)
+, _imageLoader(Ui::Emoji::CurrentSetId()) {
 	refresh();
 
 	session->data().itemRemoved(
 	) | rpl::filter([](not_null<const HistoryItem*> item) {
-		return item->isSingleEmoji();
+		return item->isIsolatedEmoji();
 	}) | rpl::start_with_next([=](not_null<const HistoryItem*> 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<HistoryItem*> item, const QString &text) {
+EmojiPack::~EmojiPack() = default;
+
+bool EmojiPack::add(not_null<HistoryItem*> 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<const HistoryItem*> item) {
-	if (!item->isSingleEmoji()) {
-		return false;
-	}
+void EmojiPack::remove(not_null<const HistoryItem*> 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<const HistoryItem*> item) {
 	if (i->second.empty()) {
 		_items.erase(i);
 	}
-	return true;
 }
 
-DocumentData *EmojiPack::stickerForEmoji(not_null<HistoryItem*> 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<Image> EmojiPack::image(const IsolatedEmoji &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;
@@ -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<not_null<HistoryItem*>> &list) {
+	for (const auto &item : list) {
 		_session->data().requestItemViewRefresh(item);
 	}
 }
@@ -172,7 +465,7 @@ base::flat_map<uint64, not_null<DocumentData*>> 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 4d5efd130..9295e3b51 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 <crl/crl_object_on_queue.h>
+
 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<Main::Session*> session);
+	~EmojiPack();
 
-	bool add(not_null<HistoryItem*> item, const QString &text);
-	bool remove(not_null<const HistoryItem*> item);
+	bool add(not_null<HistoryItem*> item);
+	void remove(not_null<const HistoryItem*> item);
 
-	[[nodiscard]] DocumentData *stickerForEmoji(not_null<HistoryItem*> item);
+	[[nodiscard]] DocumentData *stickerForEmoji(const IsolatedEmoji &emoji);
+	[[nodiscard]] std::shared_ptr<Image> 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<uint64, not_null<DocumentData*>> &map);
 	base::flat_map<uint64, not_null<DocumentData*>> collectStickers(
 		const QVector<MTPDocument> &list) const;
+	void refreshAll();
 	void refreshItems(EmojiPtr emoji);
+	void refreshItems(const base::flat_set<not_null<HistoryItem*>> &list);
 
 	not_null<Main::Session*> _session;
-	base::flat_set<not_null<HistoryItem*>> _notLoaded;
 	base::flat_map<EmojiPtr, not_null<DocumentData*>> _map;
-	base::flat_map<EmojiPtr, base::flat_set<not_null<HistoryItem*>>> _items;
+	base::flat_map<
+		IsolatedEmoji,
+		base::flat_set<not_null<HistoryItem*>>> _items;
+	base::flat_map<IsolatedEmoji, std::weak_ptr<Image>> _images;
 	mtpRequestId _requestId = 0;
 
+	crl::object_on_queue<details::EmojiImageLoader> _imageLoader;
+
 	rpl::lifetime _lifetime;
 
 };
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 7b546c7e3..7e49fdf9d 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -771,9 +771,7 @@ std::unique_ptr<HistoryView::Media> MediaFile::createView(
 	if (_document->sticker()) {
 		return std::make_unique<HistoryView::UnwrappedMedia>(
 			message,
-			std::make_unique<HistoryView::StickerContent>(
-				message,
-				_document));
+			std::make_unique<HistoryView::Sticker>(message, _document));
 	} else if (_document->isAnimation()) {
 		return std::make_unique<HistoryView::Gif>(message, _document);
 	} else if (_document->isVideoFile()) {
diff --git a/Telegram/SourceFiles/history/history.style b/Telegram/SourceFiles/history/history.style
index a9f347209..9ef27d497 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 78927694b..34803b3eb 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 417d896bf..bb065f85f 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 7b6dbe5e1..8c91de1d9 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 bf103bf11..62420ec43 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 f692baad5..a0c256852 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 57ae48d5c..5bc8eb13f 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<UnwrappedMedia>(
-			this,
-			std::make_unique<StickerContent>(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<UnwrappedMedia>(
+				this,
+				std::make_unique<Sticker>(this, document));
+		} else {
+			_media = std::make_unique<UnwrappedMedia>(
+				this,
+				std::make_unique<LargeEmoji>(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 000000000..449fbdbd8
--- /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<Image> ResolveImage(
+		not_null<Main::Session*> session,
+		const Ui::Text::IsolatedEmoji &emoji) {
+	return session->emojiStickersPack().image(emoji);
+}
+
+} // namespace
+
+LargeEmoji::LargeEmoji(
+	not_null<Element*> 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 000000000..367e282c0
--- /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<Element*> parent,
+		Ui::Text::IsolatedEmoji emoji);
+
+	QSize size() override;
+	void draw(Painter &p, const QRect &r, bool selected) override;
+
+private:
+	const not_null<Element*> _parent;
+	const Ui::Text::IsolatedEmoji _emoji;
+	std::shared_ptr<Image> _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 f21d576aa..cbf367390 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<Media> CreateAttach(
 		if (document->sticker()) {
 			return std::make_unique<UnwrappedMedia>(
 				parent,
-				std::make_unique<StickerContent>(parent, document));
+				std::make_unique<Sticker>(parent, document));
 		} else if (document->isAnimation()) {
 			return std::make_unique<Gif>(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 e6bfe61f1..60b353f37 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 8b0cd24d0..00b77454f 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 80ad17ceb..4c7d54d20 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<Main::Session*> session) {
 
 } // namespace
 
-StickerContent::StickerContent(
+Sticker::Sticker(
 	not_null<Element*> parent,
 	not_null<DocumentData*> 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 d2dddfd2b..9b454364a 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<Element*> parent,
 		not_null<DocumentData*> 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 8d01f3f84..bc76637c2 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 2b926763f..9f123ae78 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<QImage> _sprites;
-
-};
-
 auto SizeNormal = -1;
 auto SizeLarge = -1;
 auto SpritesCount = -1;
@@ -359,6 +339,71 @@ std::vector<QImage> 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 522f854bc..fb056e6a9 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<QImage> _sprites;
+
+};
+
 } // namespace Emoji
 } // namespace Ui
diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp
index 94997c0c0..99d27b60c 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<EmojiBlock*>(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 86b265a73..537c0b60d 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 <private/qfixed_p.h>
@@ -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 000000000..42bb38339
--- /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<EmojiPtr, kIsolatedEmojiLimit>;
+	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 444b191c4..02e4e2ffd 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