diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 7e33e0cb3c..9070a01bd2 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -3127,23 +3127,33 @@ void ApiWrap::sendMediaWithRandomId(
 		uint64 randomId) {
 	const auto history = item->history();
 	const auto replyTo = item->replyToId();
+
+	auto caption = item->originalText();
+	TextUtilities::Trim(caption);
+	auto sentEntities = TextUtilities::EntitiesToMTP(
+		caption.entities,
+		TextUtilities::ConvertOption::SkipLocal);
+
 	const auto flags = MTPmessages_SendMedia::Flags(0)
 		| (replyTo
 			? MTPmessages_SendMedia::Flag::f_reply_to_msg_id
 			: MTPmessages_SendMedia::Flag(0))
 		| (IsSilentPost(item, silent)
 			? MTPmessages_SendMedia::Flag::f_silent
+			: MTPmessages_SendMedia::Flag(0))
+		| (!sentEntities.v.isEmpty()
+			? MTPmessages_SendMedia::Flag::f_entities
 			: MTPmessages_SendMedia::Flag(0));
-	const auto message = QString(); // #TODO l76 caption
+
 	history->sendRequestId = request(MTPmessages_SendMedia(
 		MTP_flags(flags),
 		history->peer->input,
 		MTP_int(replyTo),
 		media,
-		MTP_string(message),
+		MTP_string(caption.text),
 		MTP_long(randomId),
 		MTPnullMarkup,
-		MTPnullEntities
+		sentEntities
 	)).done([=](const MTPUpdates &result) { applyUpdates(result);
 	}).fail([=](const RPCError &error) { sendMessageFail(error);
 	}).afterRequest(history->sendRequestId
@@ -3169,13 +3179,21 @@ void ApiWrap::sendAlbumWithUploaded(
 	Assert(itemIt != album->items.end());
 	Assert(!itemIt->media);
 
-	const auto original = item->originalText(); // #TODO l76 entities
+	auto caption = item->originalText();
+	TextUtilities::Trim(caption);
+	auto sentEntities = TextUtilities::EntitiesToMTP(
+		caption.entities,
+		TextUtilities::ConvertOption::SkipLocal);
+	const auto flags = !sentEntities.v.isEmpty()
+		? MTPDinputSingleMedia::Flag::f_entities
+		: MTPDinputSingleMedia::Flag(0);
+
 	itemIt->media = MTP_inputSingleMedia(
 		media,
-		MTP_flags(0),
+		MTP_flags(flags),
 		MTP_long(randomId),
-		MTP_string(original.text),
-		MTPnullEntities);
+		MTP_string(caption.text),
+		sentEntities);
 
 	sendAlbumIfReady(album.get());
 }
diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
index 703267c453..f5e3db2c1f 100644
--- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp
+++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
@@ -24,15 +24,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 EditCaptionBox::EditCaptionBox(
 	QWidget*,
-	not_null<Data::Media*> media,
-	FullMsgId msgId)
-: _msgId(msgId) {
-	Expects(media->allowsEditCaption());
+	not_null<HistoryItem*> item)
+: _msgId(item->fullId()) {
+	Expects(item->media() != nullptr);
+	Expects(item->media()->allowsEditCaption());
 
 	QSize dimensions;
 	ImagePtr image;
 	DocumentData *doc = nullptr;
 
+	const auto media = item->media();
 	if (const auto photo = media->photo()) {
 		_photo = true;
 		dimensions = QSize(photo->full->width(), photo->full->height());
@@ -49,7 +50,7 @@ EditCaptionBox::EditCaptionBox(
 		}
 		doc = document;
 	}
-	auto caption = media->caption();
+	auto caption = item->originalText().text;
 
 	if (!_animated && (dimensions.isEmpty() || doc || image->isNull())) {
 		if (image->isNull()) {
diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h
index eff1d52176..14c4c037f3 100644
--- a/Telegram/SourceFiles/boxes/edit_caption_box.h
+++ b/Telegram/SourceFiles/boxes/edit_caption_box.h
@@ -19,7 +19,7 @@ class InputArea;
 
 class EditCaptionBox : public BoxContent, public RPCSender {
 public:
-	EditCaptionBox(QWidget*, not_null<Data::Media*> media, FullMsgId msgId);
+	EditCaptionBox(QWidget*, not_null<HistoryItem*> item);
 
 protected:
 	void prepare() override;
diff --git a/Telegram/SourceFiles/data/data_groups.cpp b/Telegram/SourceFiles/data/data_groups.cpp
index bfdfd86952..6f9261d2fb 100644
--- a/Telegram/SourceFiles/data/data_groups.cpp
+++ b/Telegram/SourceFiles/data/data_groups.cpp
@@ -100,8 +100,7 @@ HistoryItemsList::const_iterator Groups::findPositionForItem(
 	if (!IsServerMsgId(itemId)) {
 		return last;
 	}
-	auto result = begin(group);
-	while (result != last) {
+	for (auto result = begin(group); result != last; ++result) {
 		const auto alreadyId = (*result)->id;
 		if (IsServerMsgId(alreadyId) && alreadyId > itemId) {
 			return result;
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 200ff73b20..1023b6e504 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -182,10 +182,6 @@ bool Media::canBeGrouped() const {
 	return false;
 }
 
-QString Media::caption() const {
-	return QString();
-}
-
 QString Media::chatsListText() const {
 	auto result = notificationText();
 	return result.isEmpty()
@@ -236,11 +232,9 @@ std::unique_ptr<HistoryMedia> Media::createView(
 
 MediaPhoto::MediaPhoto(
 	not_null<HistoryItem*> parent,
-	not_null<PhotoData*> photo,
-	const QString &caption)
+	not_null<PhotoData*> photo)
 : Media(parent)
-, _photo(photo)
-, _caption(caption) {
+, _photo(photo) {
 }
 
 MediaPhoto::MediaPhoto(
@@ -258,7 +252,7 @@ MediaPhoto::~MediaPhoto() {
 std::unique_ptr<Media> MediaPhoto::clone(not_null<HistoryItem*> parent) {
 	return _chat
 		? std::make_unique<MediaPhoto>(parent, _chat, _photo)
-		: std::make_unique<MediaPhoto>(parent, _photo, _caption);
+		: std::make_unique<MediaPhoto>(parent, _photo);
 }
 
 PhotoData *MediaPhoto::photo() const {
@@ -283,17 +277,17 @@ bool MediaPhoto::canBeGrouped() const {
 	return true;
 }
 
-QString MediaPhoto::caption() const {
-	return _caption;
-}
-
 QString MediaPhoto::notificationText() const {
-	return WithCaptionNotificationText(lang(lng_in_dlg_photo), _caption);
+	return WithCaptionNotificationText(
+		lang(lng_in_dlg_photo),
+		parent()->originalText().text);
 	//return WithCaptionNotificationText(lang(lng_in_dlg_album), _caption);
 }
 
 QString MediaPhoto::chatsListText() const {
-	return WithCaptionDialogsText(lang(lng_in_dlg_photo), _caption);
+	return WithCaptionDialogsText(
+		lang(lng_in_dlg_photo),
+		parent()->originalText().text);
 	//return WithCaptionDialogsText(lang(lng_in_dlg_album), _caption);
 }
 
@@ -408,17 +402,14 @@ std::unique_ptr<HistoryMedia> MediaPhoto::createView(
 	return std::make_unique<HistoryPhoto>(
 		message,
 		realParent,
-		_photo,
-		_caption);
+		_photo);
 }
 
 MediaFile::MediaFile(
 	not_null<HistoryItem*> parent,
-	not_null<DocumentData*> document,
-	const QString &caption)
+	not_null<DocumentData*> document)
 : Media(parent)
 , _document(document)
-, _caption(caption)
 , _emoji(document->sticker() ? document->sticker()->alt : QString()) {
 	Auth().data().registerDocumentItem(_document, parent);
 
@@ -434,7 +425,7 @@ MediaFile::~MediaFile() {
 }
 
 std::unique_ptr<Media> MediaFile::clone(not_null<HistoryItem*> parent) {
-	return std::make_unique<MediaFile>(parent, _document, _caption);
+	return std::make_unique<MediaFile>(parent, _document);
 }
 
 DocumentData *MediaFile::document() const {
@@ -493,7 +484,7 @@ QString MediaFile::chatsListText() const {
 		}
 		return lang(lng_in_dlg_file);
 	}();
-	return WithCaptionDialogsText(type, _caption);
+	return WithCaptionDialogsText(type, parent()->originalText().text);
 }
 
 QString MediaFile::notificationText() const {
@@ -518,7 +509,7 @@ QString MediaFile::notificationText() const {
 		}
 		return lang(lng_in_dlg_file);
 	}();
-	return WithCaptionNotificationText(type, _caption);
+	return WithCaptionNotificationText(type, parent()->originalText().text);
 }
 
 QString MediaFile::pinnedTextSubstring() const {
@@ -622,15 +613,14 @@ std::unique_ptr<HistoryMedia> MediaFile::createView(
 	if (_document->sticker()) {
 		return std::make_unique<HistorySticker>(message, _document);
 	} else if (_document->isAnimation()) {
-		return std::make_unique<HistoryGif>(message, _document, _caption);
+		return std::make_unique<HistoryGif>(message, _document);
 	} else if (_document->isVideoFile()) {
 		return std::make_unique<HistoryVideo>(
 			message,
 			realParent,
-			_document,
-			_caption);
+			_document);
 	}
-	return std::make_unique<HistoryDocument>(message, _document, _caption);
+	return std::make_unique<HistoryDocument>(message, _document);
 }
 
 MediaContact::MediaContact(
@@ -847,8 +837,12 @@ WebPageData *MediaWebPage::webpage() const {
 	return _page;
 }
 
+QString MediaWebPage::chatsListText() const {
+	return notificationText();
+}
+
 QString MediaWebPage::notificationText() const {
-	return QString();
+	return parent()->originalText().text;
 }
 
 QString MediaWebPage::pinnedTextSubstring() const {
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 8c30e901c8..dc10632fa9 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -82,7 +82,6 @@ public:
 	virtual bool uploading() const;
 	virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;
 	virtual bool canBeGrouped() const;
-	virtual QString caption() const;
 	virtual bool hasReplyPreview() const;
 	virtual ImagePtr replyPreview() const;
 	// Returns text with link-start and link-end commands for service-color highlighting.
@@ -120,8 +119,7 @@ class MediaPhoto : public Media {
 public:
 	MediaPhoto(
 		not_null<HistoryItem*> parent,
-		not_null<PhotoData*> photo,
-		const QString &caption);
+		not_null<PhotoData*> photo);
 	MediaPhoto(
 		not_null<HistoryItem*> parent,
 		not_null<PeerData*> chat,
@@ -135,7 +133,6 @@ public:
 	bool uploading() const override;
 	Storage::SharedMediaTypesMask sharedMediaTypes() const override;
 	bool canBeGrouped() const override;
-	QString caption() const override;
 	QString chatsListText() const override;
 	QString notificationText() const override;
 	QString pinnedTextSubstring() const override;
@@ -152,7 +149,6 @@ public:
 private:
 	not_null<PhotoData*> _photo;
 	PeerData *_chat = nullptr;
-	QString _caption;
 
 };
 
@@ -160,8 +156,7 @@ class MediaFile : public Media {
 public:
 	MediaFile(
 		not_null<HistoryItem*> parent,
-		not_null<DocumentData*> document,
-		const QString &caption);
+		not_null<DocumentData*> document);
 	~MediaFile();
 
 	std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
@@ -187,7 +182,6 @@ public:
 
 private:
 	not_null<DocumentData*> _document;
-	QString _caption;
 	QString _emoji;
 
 };
@@ -289,6 +283,7 @@ public:
 	std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
 
 	WebPageData *webpage() const override;
+	QString chatsListText() const override;
 	QString notificationText() const override;
 	QString pinnedTextSubstring() const override;
 	bool allowsEdit() const override;
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index e7194f2bbb..974cca01a1 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -208,14 +208,14 @@ rpl::producer<not_null<ViewElement*>> Session::viewResizeRequest() const {
 	return _viewResizeRequest.events();
 }
 
-void Session::requestItemViewRefresh(not_null<const HistoryItem*> item) {
+void Session::requestItemViewRefresh(not_null<HistoryItem*> item) {
 	if (const auto view = item->mainView()) {
 		view->setPendingResize();
 	}
 	_itemViewRefreshRequest.fire_copy(item);
 }
 
-rpl::producer<not_null<const HistoryItem*>> Session::itemViewRefreshRequest() const {
+rpl::producer<not_null<HistoryItem*>> Session::itemViewRefreshRequest() const {
 	return _itemViewRefreshRequest.events();
 }
 
diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h
index 27bfb84b01..e6f2bf0a3a 100644
--- a/Telegram/SourceFiles/data/data_session.h
+++ b/Telegram/SourceFiles/data/data_session.h
@@ -73,8 +73,8 @@ public:
 	rpl::producer<not_null<const HistoryItem*>> itemResizeRequest() const;
 	void requestViewResize(not_null<ViewElement*> view);
 	rpl::producer<not_null<ViewElement*>> viewResizeRequest() const;
-	void requestItemViewRefresh(not_null<const HistoryItem*> item);
-	rpl::producer<not_null<const HistoryItem*>> itemViewRefreshRequest() const;
+	void requestItemViewRefresh(not_null<HistoryItem*> item);
+	rpl::producer<not_null<HistoryItem*>> itemViewRefreshRequest() const;
 	void requestItemPlayInline(not_null<const HistoryItem*> item);
 	rpl::producer<not_null<const HistoryItem*>> itemPlayInlineRequest() const;
 	void notifyHistoryUnloaded(not_null<const History*> history);
@@ -448,7 +448,7 @@ private:
 	rpl::event_stream<not_null<const ViewElement*>> _viewRepaintRequest;
 	rpl::event_stream<not_null<const HistoryItem*>> _itemResizeRequest;
 	rpl::event_stream<not_null<ViewElement*>> _viewResizeRequest;
-	rpl::event_stream<not_null<const HistoryItem*>> _itemViewRefreshRequest;
+	rpl::event_stream<not_null<HistoryItem*>> _itemViewRefreshRequest;
 	rpl::event_stream<not_null<const HistoryItem*>> _itemPlayInlineRequest;
 	rpl::event_stream<not_null<const HistoryItem*>> _itemRemoved;
 	rpl::event_stream<not_null<const History*>> _historyUnloaded;
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
index 87d43b35b2..8648b5e8ee 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
@@ -974,7 +974,7 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 						}
 					}
 				}
-				if (msg && !_contextMenuLink && (!msg->emptyText() || mediaHasTextForCopy)) {
+				if (msg && !_contextMenuLink && (view->hasVisibleText() || mediaHasTextForCopy)) {
 					_menu->addAction(lang(lng_context_copy_text), [=] {
 						copyContextText(itemId);
 					})->setEnabled(true);
diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index 5c0a90ec32..ac099511c3 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -785,7 +785,7 @@ not_null<HistoryItem*> History::createItemDocument(
 		UserId from,
 		const QString &postAuthor,
 		DocumentData *document,
-		const QString &caption,
+		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup) {
 	return HistoryMessage::create(
 		this,
@@ -810,7 +810,7 @@ not_null<HistoryItem*> History::createItemPhoto(
 		UserId from,
 		const QString &postAuthor,
 		PhotoData *photo,
-		const QString &caption,
+		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup) {
 	return HistoryMessage::create(
 		this,
@@ -933,7 +933,7 @@ not_null<HistoryItem*> History::addNewDocument(
 		UserId from,
 		const QString &postAuthor,
 		DocumentData *document,
-		const QString &caption,
+		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup) {
 	return addNewItem(
 		createItemDocument(
@@ -959,7 +959,7 @@ not_null<HistoryItem*> History::addNewPhoto(
 		UserId from,
 		const QString &postAuthor,
 		PhotoData *photo,
-		const QString &caption,
+		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup) {
 	return addNewItem(
 		createItemPhoto(
@@ -2494,6 +2494,7 @@ void History::clearUpTill(MsgId availableMinId) {
 	if (!lastMsg) {
 		App::main()->checkPeerHistory(peer);
 	}
+	Auth().data().sendHistoryChangeNotifications();
 }
 
 void History::applyGroupAdminChanges(
@@ -2626,6 +2627,29 @@ void HistoryBlock::remove(not_null<Element*> view) {
 	}
 }
 
+void HistoryBlock::refreshView(not_null<Element*> view) {
+	Expects(view->block() == this);
+
+	const auto item = view->data();
+	auto refreshed = item->createView(HistoryInner::ElementDelegate());
+
+	auto blockIndex = indexInHistory();
+	auto itemIndex = view->indexInBlock();
+	if (_history->scrollTopItem == view) {
+		_history->scrollTopItem = refreshed.get();
+	}
+
+	messages[itemIndex] = std::move(refreshed);
+	messages[itemIndex]->attachToBlock(this, itemIndex);
+	if (itemIndex + 1 < messages.size()) {
+		messages[itemIndex + 1]->previousInBlocksChanged();
+	} else if (blockIndex + 1 < _history->blocks.size()) {
+		_history->blocks[blockIndex + 1]->messages.front()->previousInBlocksChanged();
+	} else if (!_history->blocks.empty() && !_history->blocks.back()->messages.empty()) {
+		_history->blocks.back()->messages.back()->nextInBlocksChanged();
+	}
+}
+
 HistoryBlock::~HistoryBlock() {
 	clear();
 }
diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h
index 186ba1fc3e..911e391e0c 100644
--- a/Telegram/SourceFiles/history/history.h
+++ b/Telegram/SourceFiles/history/history.h
@@ -159,8 +159,8 @@ public:
 	HistoryItem *addToHistory(const MTPMessage &msg);
 	not_null<HistoryItem*> addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags = 0, bool newMsg = true);
 	not_null<HistoryItem*> addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *item);
-	not_null<HistoryItem*> addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup);
-	not_null<HistoryItem*> addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup);
+	not_null<HistoryItem*> addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const TextWithEntities &caption, const MTPReplyMarkup &markup);
+	not_null<HistoryItem*> addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const TextWithEntities &caption, const MTPReplyMarkup &markup);
 	not_null<HistoryItem*> addNewGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup);
 
 	// Used only internally and for channel admin log.
@@ -400,8 +400,8 @@ protected:
 	void clearBlocks(bool leaveItems);
 
 	not_null<HistoryItem*> createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *msg);
-	not_null<HistoryItem*> createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup);
-	not_null<HistoryItem*> createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup);
+	not_null<HistoryItem*> createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const TextWithEntities &caption, const MTPReplyMarkup &markup);
+	not_null<HistoryItem*> createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const TextWithEntities &caption, const MTPReplyMarkup &markup);
 	not_null<HistoryItem*> createItemGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup);
 
 	not_null<HistoryItem*> addNewItem(
@@ -559,6 +559,7 @@ public:
 
 	void clear(bool leaveItems = false);
 	void remove(not_null<Element*> view);
+	void refreshView(not_null<Element*> view);
 
 	int resizeGetHeight(int newWidth, bool resizeAllItems);
 	int y() const {
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 2c8e8203c4..5dca6465b5 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -1542,7 +1542,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 						}
 					}
 				}
-				if (msg && !_contextMenuLink && (!msg->emptyText() || mediaHasTextForCopy)) {
+				if (msg && view && !_contextMenuLink && (view->hasVisibleText() || mediaHasTextForCopy)) {
 					_menu->addAction(lang(lng_context_copy_text), [=] {
 						copyContextText(itemId);
 					})->setEnabled(true);
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 33f203c3ff..27438a2c1e 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -90,15 +90,13 @@ void HistoryItem::finishCreate() {
 void HistoryItem::finishEdition(int oldKeyboardTop) {
 	Auth().data().requestItemViewRefresh(this);
 	invalidateChatsListEntry();
-	//if (groupId()) {
-	//	history()->fixGroupAfterEdition(this);
-	//}
-	//if (isHiddenByGroup()) { // #TODO group views
-	//	// Perhaps caption was changed, we should refresh the group.
-	//	const auto group = Get<HistoryMessageGroup>();
-	//	group->leader->setPendingInitDimensions();
-	//	group->leader->invalidateChatsListEntry();
-	//}
+	if (const auto group = Auth().data().groups().find(this)) {
+		const auto leader = group->items.back();
+		if (leader != this) {
+			Auth().data().requestItemViewRefresh(leader);
+			leader->invalidateChatsListEntry();
+		}
+	}
 
 	//if (oldKeyboardTop >= 0) { // #TODO edit bot message
 	//	if (auto keyboard = Get<HistoryMessageReplyMarkup>()) {
@@ -290,6 +288,13 @@ void HistoryItem::destroy() {
 	delete this;
 }
 
+void HistoryItem::refreshMainView() {
+	if (const auto view = mainView()) {
+		Auth().data().notifyHistoryChangeDelayed(_history);
+		view->refreshInBlock();
+	}
+}
+
 void HistoryItem::removeMainView() {
 	if (const auto view = mainView()) {
 		if (const auto channelHistory = _history->asChannelHistory()) {
@@ -297,7 +302,6 @@ void HistoryItem::removeMainView() {
 		}
 		Auth().data().notifyHistoryChangeDelayed(_history);
 		view->removeFromBlock();
-		_mainView = nullptr;
 	}
 }
 
@@ -677,10 +681,12 @@ HistoryItem *HistoryItem::nextItem() const {
 
 QString HistoryItem::notificationText() const {
 	auto getText = [this]() {
-		if (emptyText()) {
-			return _media ? _media->notificationText() : QString();
+		if (_media) {
+			return _media->notificationText();
+		} else if (!emptyText()) {
+			return _text.originalText();
 		}
-		return _text.originalText();
+		return QString();
 	};
 
 	auto result = getText();
@@ -692,10 +698,12 @@ QString HistoryItem::notificationText() const {
 
 QString HistoryItem::inDialogsText(DrawInDialog way) const {
 	auto getText = [this]() {
-		if (emptyText()) {
-			return _media ? _media->chatsListText() : QString();
+		if (_media) {
+			return _media->chatsListText();
+		} else if (!emptyText()) {
+			return TextUtilities::Clean(_text.originalText());
 		}
-		return TextUtilities::Clean(_text.originalText());
+		return QString();
 	};
 	const auto plainText = getText();
 	const auto sender = [&]() -> PeerData* {
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index 6d208d35b6..4ac0df8a07 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -95,6 +95,7 @@ public:
 	void setMainView(HistoryView::Element *view) {
 		_mainView = view;
 	}
+	void refreshMainView();
 	void clearMainView();
 	void removeMainView();
 
@@ -196,6 +197,9 @@ public:
 	bool emptyText() const {
 		return _text.isEmpty();
 	}
+	Text cloneText() const {
+		return _text;
+	}
 
 	bool isPinned() const;
 	bool canPin() const;
diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp
index f007626900..312a438b55 100644
--- a/Telegram/SourceFiles/history/history_item_components.cpp
+++ b/Telegram/SourceFiles/history/history_item_components.cpp
@@ -271,7 +271,7 @@ void HistoryMessageReply::paint(
 
 				auto replyToAsMsg = replyToMsg->toHistoryMessage();
 				if (!(flags & PaintFlag::InBubble)) {
-				} else if ((replyToAsMsg && replyToAsMsg->emptyText()) || replyToMsg->serviceMsg()) {
+				} else if (!replyToAsMsg) {
 					p.setPen(outbg ? (selected ? st::msgOutDateFgSelected : st::msgOutDateFg) : (selected ? st::msgInDateFgSelected : st::msgInDateFg));
 				} else {
 					p.setPen(outbg ? (selected ? st::historyTextOutFgSelected : st::historyTextOutFg) : (selected ? st::historyTextInFgSelected : st::historyTextInFg));
diff --git a/Telegram/SourceFiles/history/history_media.h b/Telegram/SourceFiles/history/history_media.h
index 4ba5970380..0cc21f2cce 100644
--- a/Telegram/SourceFiles/history/history_media.h
+++ b/Telegram/SourceFiles/history/history_media.h
@@ -74,6 +74,9 @@ public:
 	virtual bool hasTextForCopy() const {
 		return false;
 	}
+	virtual bool hideMessageText() const {
+		return true;
+	}
 	virtual bool allowsFastShare() const {
 		return false;
 	}
diff --git a/Telegram/SourceFiles/history/history_media_grouped.cpp b/Telegram/SourceFiles/history/history_media_grouped.cpp
index 43bf630c10..34f4410fe4 100644
--- a/Telegram/SourceFiles/history/history_media_grouped.cpp
+++ b/Telegram/SourceFiles/history/history_media_grouped.cpp
@@ -367,29 +367,20 @@ HistoryMessageEdited *HistoryGroupedMedia::displayedEditBadge() const {
 }
 
 void HistoryGroupedMedia::updateNeedBubbleState() {
-	const auto getItemCaption = [](const Part &part) {
-		if (const auto media = part.item->media()) {
-			return TextWithEntities{ media->caption(), EntitiesInText() };
-			// #TODO group caption
-		}
-		return part.content->getCaption();
-	};
-	const auto captionText = [&] {
-		auto result = getItemCaption(_parts.front());
-		if (result.text.isEmpty()) {
-			return result;
+	const auto hasCaption = [&] {
+		if (_parts.front().item->emptyText()) {
+			return false;
 		}
 		for (auto i = 1, count = int(_parts.size()); i != count; ++i) {
-			if (!getItemCaption(_parts[i]).text.isEmpty()) {
-				return TextWithEntities();
+			if (!_parts[i].item->emptyText()) {
+				return false;
 			}
 		}
-		return result;
+		return true;
 	}();
-	_caption.setText(
-		st::messageTextStyle,
-		captionText.text + _parent->skipBlock(),
-		Ui::ItemTextNoMonoOptions(_parent->data()));
+	if (hasCaption) {
+		_caption = _parts.front().item->cloneText();
+	}
 	_needBubble = computeNeedBubble();
 }
 
diff --git a/Telegram/SourceFiles/history/history_media_types.cpp b/Telegram/SourceFiles/history/history_media_types.cpp
index da72f82f62..b01bfb73e2 100644
--- a/Telegram/SourceFiles/history/history_media_types.cpp
+++ b/Telegram/SourceFiles/history/history_media_types.cpp
@@ -72,25 +72,21 @@ std::unique_ptr<HistoryMedia> CreateAttach(
 		} else if (document->isAnimation()) {
 			return std::make_unique<HistoryGif>(
 				parent,
-				document,
-				QString());
+				document);
 		} else if (document->isVideoFile()) {
 			return std::make_unique<HistoryVideo>(
 				parent,
 				parent->data(),
-				document,
-				QString());
+				document);
 		}
 		return std::make_unique<HistoryDocument>(
 			parent,
-			document,
-			QString());
+			document);
 	} else if (photo) {
 		return std::make_unique<HistoryPhoto>(
 			parent,
 			parent->data(),
-			photo,
-			QString());
+			photo);
 	}
 	return nullptr;
 }
@@ -223,8 +219,7 @@ HistoryFileMedia::~HistoryFileMedia() = default;
 HistoryPhoto::HistoryPhoto(
 	not_null<Element*> parent,
 	not_null<HistoryItem*> realParent,
-	not_null<PhotoData*> photo,
-	const QString &caption)
+	not_null<PhotoData*> photo)
 : HistoryFileMedia(parent)
 , _data(photo)
 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) {
@@ -233,12 +228,7 @@ HistoryPhoto::HistoryPhoto(
 		std::make_shared<PhotoOpenClickHandler>(_data, fullId),
 		std::make_shared<PhotoSaveClickHandler>(_data, fullId),
 		std::make_shared<PhotoCancelClickHandler>(_data, fullId));
-	if (!caption.isEmpty()) {
-		_caption.setText(
-			st::messageTextStyle,
-			caption + _parent->skipBlock(),
-			Ui::ItemTextNoMonoOptions(_parent->data()));
-	}
+	_caption = realParent->cloneText();
 	create(realParent->fullId());
 }
 
@@ -263,7 +253,9 @@ void HistoryPhoto::create(FullMsgId contextId, PeerData *chat) {
 }
 
 QSize HistoryPhoto::countOptimalSize() {
-	if (_caption.hasSkipBlock()) {
+	if (_parent->media() != this) {
+		_caption = Text();
+	} else if (_caption.hasSkipBlock()) {
 		_caption.updateSkipBlock(
 			_parent->skipBlockWidth(),
 			_parent->skipBlockHeight());
@@ -733,19 +725,13 @@ DocumentViewRegister::~DocumentViewRegister() {
 HistoryVideo::HistoryVideo(
 	not_null<Element*> parent,
 	not_null<HistoryItem*> realParent,
-	not_null<DocumentData*> document,
-	const QString &caption)
+	not_null<DocumentData*> document)
 : HistoryFileMedia(parent)
 , DocumentViewRegister(parent, document)
 , _data(document)
 , _thumbw(1)
 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) {
-	if (!caption.isEmpty()) {
-		_caption.setText(
-			st::messageTextStyle,
-			caption + _parent->skipBlock(),
-			Ui::ItemTextNoMonoOptions(_parent->data()));
-	}
+	_caption = realParent->cloneText();
 
 	setDocumentLinks(_data, realParent);
 
@@ -755,7 +741,9 @@ HistoryVideo::HistoryVideo(
 }
 
 QSize HistoryVideo::countOptimalSize() {
-	if (_caption.hasSkipBlock()) {
+	if (_parent->media() != this) {
+		_caption = Text();
+	} else if (_caption.hasSkipBlock()) {
 		_caption.updateSkipBlock(
 			_parent->skipBlockWidth(),
 			_parent->skipBlockHeight());
@@ -1206,12 +1194,12 @@ ImagePtr HistoryVideo::replyPreview() {
 
 HistoryDocument::HistoryDocument(
 	not_null<Element*> parent,
-	not_null<DocumentData*> document,
-	const QString &caption)
+	not_null<DocumentData*> document)
 : HistoryFileMedia(parent)
 , DocumentViewRegister(parent, document)
 , _data(document) {
 	const auto item = parent->data();
+	auto caption = item->cloneText();
 
 	createComponents(!caption.isEmpty());
 	if (auto named = Get<HistoryDocumentNamed>()) {
@@ -1223,10 +1211,7 @@ HistoryDocument::HistoryDocument(
 	setStatusSize(FileStatusSizeReady);
 
 	if (auto captioned = Get<HistoryDocumentCaptioned>()) {
-		captioned->_caption.setText(
-			st::messageTextStyle,
-			caption + _parent->skipBlock(),
-			Ui::ItemTextNoMonoOptions(item));
+		captioned->_caption = std::move(caption);
 	}
 }
 
@@ -1284,12 +1269,16 @@ QSize HistoryDocument::countOptimalSize() {
 	const auto item = _parent->data();
 
 	auto captioned = Get<HistoryDocumentCaptioned>();
-	if (captioned && captioned->_caption.hasSkipBlock()) {
+	if (_parent->media() != this) {
+		if (captioned) {
+			RemoveComponents(HistoryDocumentCaptioned::Bit());
+			captioned = nullptr;
+		}
+	} else if (captioned && captioned->_caption.hasSkipBlock()) {
 		captioned->_caption.updateSkipBlock(
 			_parent->skipBlockWidth(),
 			_parent->skipBlockHeight());
 	}
-
 	auto thumbed = Get<HistoryDocumentThumbed>();
 	if (thumbed) {
 		_data->thumb->load();
@@ -1953,8 +1942,7 @@ ImagePtr HistoryDocument::replyPreview() {
 
 HistoryGif::HistoryGif(
 	not_null<Element*> parent,
-	not_null<DocumentData*> document,
-	const QString &caption)
+	not_null<DocumentData*> document)
 : HistoryFileMedia(parent)
 , DocumentViewRegister(parent, document)
 , _data(document)
@@ -1964,18 +1952,14 @@ HistoryGif::HistoryGif(
 
 	setStatusSize(FileStatusSizeReady);
 
-	if (!caption.isEmpty() && !_data->isVideoMessage()) {
-		_caption.setText(
-			st::messageTextStyle,
-			caption + _parent->skipBlock(),
-			Ui::ItemTextNoMonoOptions(item));
-	}
-
+	_caption = item->cloneText();
 	_data->thumb->load();
 }
 
 QSize HistoryGif::countOptimalSize() {
-	if (_caption.hasSkipBlock()) {
+	if (_parent->media() != this) {
+		_caption = Text();
+	} else if (_caption.hasSkipBlock()) {
 		_caption.updateSkipBlock(
 			_parent->skipBlockWidth(),
 			_parent->skipBlockHeight());
diff --git a/Telegram/SourceFiles/history/history_media_types.h b/Telegram/SourceFiles/history/history_media_types.h
index 5eee32f5d8..6e56fbab44 100644
--- a/Telegram/SourceFiles/history/history_media_types.h
+++ b/Telegram/SourceFiles/history/history_media_types.h
@@ -130,8 +130,7 @@ public:
 	HistoryPhoto(
 		not_null<Element*> parent,
 		not_null<HistoryItem*> realParent,
-		not_null<PhotoData*> photo,
-		const QString &caption);
+		not_null<PhotoData*> photo);
 	HistoryPhoto(
 		not_null<Element*> parent,
 		not_null<PeerData*> chat,
@@ -243,8 +242,7 @@ public:
 	HistoryVideo(
 		not_null<Element*> parent,
 		not_null<HistoryItem*> realParent,
-		not_null<DocumentData*> document,
-		const QString &caption);
+		not_null<DocumentData*> document);
 
 	HistoryMediaType type() const override {
 		return MediaTypeVideo;
@@ -336,8 +334,7 @@ class HistoryDocument
 public:
 	HistoryDocument(
 		not_null<Element*> parent,
-		not_null<DocumentData*> document,
-		const QString &caption);
+		not_null<DocumentData*> document);
 
 	HistoryMediaType type() const override {
 		return _data->isVoiceMessage()
@@ -407,8 +404,8 @@ private:
 	void setStatusSize(int newSize, qint64 realDuration = 0) const;
 	bool updateStatusText() const; // returns showPause
 
-								   // Callback is a void(const QString &, const QString &, const Text &) functor.
-								   // It will be called as callback(attachType, attachFileName, attachCaption).
+	// Callback is a void(const QString &, const QString &, const Text &) functor.
+	// It will be called as callback(attachType, attachFileName, attachCaption).
 	template <typename Callback>
 	void buildStringRepresentation(Callback callback) const;
 
@@ -420,8 +417,7 @@ class HistoryGif : public HistoryFileMedia, public DocumentViewRegister {
 public:
 	HistoryGif(
 		not_null<Element*> parent,
-		not_null<DocumentData*> document,
-		const QString &caption);
+		not_null<DocumentData*> document);
 
 	HistoryMediaType type() const override {
 		return MediaTypeGif;
@@ -703,6 +699,10 @@ public:
 	void draw(Painter &p, const QRect &r, TextSelection selection, TimeMs ms) const override;
 	HistoryTextState getState(QPoint point, HistoryStateRequest request) const override;
 
+	bool hideMessageText() const override {
+		return false;
+	}
+
 	[[nodiscard]] TextSelection adjustSelection(
 		TextSelection selection,
 		TextSelectType type) const override;
@@ -918,6 +918,10 @@ public:
 	}
 	static QString fillAmountAndCurrency(uint64 amount, const QString &currency);
 
+	bool hideMessageText() const override {
+		return false;
+	}
+
 	void draw(Painter &p, const QRect &r, TextSelection selection, TimeMs ms) const override;
 	HistoryTextState getState(QPoint point, HistoryStateRequest request) const override;
 
diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp
index 289dc1aabb..794661c04a 100644
--- a/Telegram/SourceFiles/history/history_message.cpp
+++ b/Telegram/SourceFiles/history/history_message.cpp
@@ -308,9 +308,6 @@ HistoryMessage::HistoryMessage(
 	if (msg.has_reply_markup()) config.mtpMarkup = &msg.vreply_markup;
 	if (msg.has_edit_date()) config.editDate = ::date(msg.vedit_date);
 	if (msg.has_post_author()) config.author = qs(msg.vpost_author);
-	if (msg.has_grouped_id()) {
-		setGroupId(MessageGroupId::FromRaw(msg.vgrouped_id.v));
-	}
 
 	createComponents(config);
 
@@ -323,6 +320,10 @@ HistoryMessage::HistoryMessage(
 		? TextUtilities::EntitiesFromMTP(msg.ventities.v)
 		: EntitiesInText();
 	setText({ text, entities });
+
+	if (msg.has_grouped_id()) {
+		setGroupId(MessageGroupId::FromRaw(msg.vgrouped_id.v));
+	}
 }
 
 HistoryMessage::HistoryMessage(
@@ -444,13 +445,13 @@ HistoryMessage::HistoryMessage(
 	UserId from,
 	const QString &postAuthor,
 	not_null<DocumentData*> document,
-	const QString &caption,
+	const TextWithEntities &caption,
 	const MTPReplyMarkup &markup)
 : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) {
 	createComponentsHelper(flags, replyTo, viaBotId, postAuthor, markup);
 
-	_media = std::make_unique<Data::MediaFile>(this, document, caption);
-	setText(TextWithEntities());
+	_media = std::make_unique<Data::MediaFile>(this, document);
+	setText(caption);
 }
 
 HistoryMessage::HistoryMessage(
@@ -463,13 +464,13 @@ HistoryMessage::HistoryMessage(
 	UserId from,
 	const QString &postAuthor,
 	not_null<PhotoData*> photo,
-	const QString &caption,
+	const TextWithEntities &caption,
 	const MTPReplyMarkup &markup)
 : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) {
 	createComponentsHelper(flags, replyTo, viaBotId, postAuthor, markup);
 
-	_media = std::make_unique<Data::MediaPhoto>(this, photo, caption);
-	setText(TextWithEntities());
+	_media = std::make_unique<Data::MediaPhoto>(this, photo);
+	setText(caption);
 }
 
 HistoryMessage::HistoryMessage(
@@ -705,11 +706,15 @@ QString FormatViewsCount(int views) {
 }
 
 void HistoryMessage::refreshMedia(const MTPMessageMedia *media) {
-	const auto wasGrouped = Auth().data().groups().isGrouped(this);
 	_media = nullptr;
 	if (media) {
 		setMedia(*media);
 	}
+}
+
+void HistoryMessage::refreshSentMedia(const MTPMessageMedia *media) {
+	const auto wasGrouped = Auth().data().groups().isGrouped(this);
+	refreshMedia(media);
 	if (wasGrouped) {
 		Auth().data().groups().refreshMessage(this);
 	}
@@ -772,8 +777,7 @@ std::unique_ptr<Data::Media> HistoryMessage::CreateMedia(
 		} else if (data.has_photo() && data.vphoto.type() == mtpc_photo) {
 			return std::make_unique<Data::MediaPhoto>(
 				item,
-				Auth().data().photo(data.vphoto.c_photo()),
-				/*data.has_caption() ? qs(data.vcaption) : */QString()); // #TODO l76 caption
+				Auth().data().photo(data.vphoto.c_photo()));
 		} else {
 			LOG(("API Error: "
 				"Got MTPMessageMediaPhoto "
@@ -790,8 +794,7 @@ std::unique_ptr<Data::Media> HistoryMessage::CreateMedia(
 			&& data.vdocument.type() == mtpc_document) {
 			return std::make_unique<Data::MediaFile>(
 				item,
-				Auth().data().document(data.vdocument.c_document()),
-				/*data.has_caption() ? qs(data.vcaption) :*/ QString()); // #TODO l76 caption
+				Auth().data().document(data.vdocument.c_document()));
 		} else {
 			LOG(("API Error: "
 				"Got MTPMessageMediaDocument "
@@ -900,12 +903,12 @@ void HistoryMessage::applyEditionToEmpty() {
 void HistoryMessage::updateSentMedia(const MTPMessageMedia *media) {
 	if (_flags & MTPDmessage_ClientFlag::f_from_inline_bot) {
 		if (!media || !_media || !_media->updateInlineResultMedia(*media)) {
-			refreshMedia(media);
+			refreshSentMedia(media);
 		}
 		_flags &= ~MTPDmessage_ClientFlag::f_from_inline_bot;
 	} else {
 		if (!media || !_media || !_media->updateSentMedia(*media)) {
-			refreshMedia(media);
+			refreshSentMedia(media);
 		}
 	}
 	Auth().data().requestItemResize(this);
diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h
index dbdfa26a97..2bdf89b933 100644
--- a/Telegram/SourceFiles/history/history_message.h
+++ b/Telegram/SourceFiles/history/history_message.h
@@ -79,7 +79,7 @@ public:
 			UserId from,
 			const QString &postAuthor,
 			not_null<DocumentData*> document,
-			const QString &caption,
+			const TextWithEntities &caption,
 			const MTPReplyMarkup &markup) {
 		return _create(
 			history,
@@ -104,7 +104,7 @@ public:
 			UserId from,
 			const QString &postAuthor,
 			not_null<PhotoData*> photo,
-			const QString &caption,
+			const TextWithEntities &caption,
 			const MTPReplyMarkup &markup) {
 		return _create(
 			history,
@@ -144,6 +144,7 @@ public:
 	}
 
 	void refreshMedia(const MTPMessageMedia *media);
+	void refreshSentMedia(const MTPMessageMedia *media);
 	void setMedia(const MTPMessageMedia &media);
 	static std::unique_ptr<Data::Media> CreateMedia(
 		not_null<HistoryMessage*> item,
@@ -234,7 +235,7 @@ private:
 		UserId from,
 		const QString &postAuthor,
 		not_null<DocumentData*> document,
-		const QString &caption,
+		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup); // local document
 	HistoryMessage(
 		not_null<History*> history,
@@ -246,7 +247,7 @@ private:
 		UserId from,
 		const QString &postAuthor,
 		not_null<PhotoData*> photo,
-		const QString &caption,
+		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup); // local photo
 	HistoryMessage(
 		not_null<History*> history,
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 3e99a62550..02c6fe849e 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -571,10 +571,8 @@ HistoryWidget::HistoryWidget(QWidget *parent, not_null<Window::Controller*> cont
 	}, lifetime());
 	Auth().data().itemViewRefreshRequest(
 	) | rpl::start_with_next([this](auto item) {
-		if (const auto view = item->mainView()) {
-			updateHistoryGeometry();
-		}
-	});
+		item->refreshMainView();
+	}, lifetime());
 	subscribe(Auth().data().contactsLoaded(), [this](bool) {
 		if (_peer) {
 			updateReportSpamStatus();
@@ -4288,7 +4286,7 @@ void HistoryWidget::sendFileConfirmed(
 				MTP_string(file->caption),
 				photo,
 				MTPnullMarkup,
-				MTPnullEntities,// #TODO l76 entities
+				MTPnullEntities, // #TODO caption entities
 				MTP_int(1),
 				MTPint(),
 				MTP_string(messagePostAuthor),
@@ -4313,7 +4311,7 @@ void HistoryWidget::sendFileConfirmed(
 				MTP_string(file->caption),
 				document,
 				MTPnullMarkup,
-				MTPnullEntities, // #TODO l76 entities
+				MTPnullEntities, // #TODO caption entities
 				MTP_int(1),
 				MTPint(),
 				MTP_string(messagePostAuthor),
@@ -4341,7 +4339,7 @@ void HistoryWidget::sendFileConfirmed(
 				MTP_string(file->caption),
 				document,
 				MTPnullMarkup,
-				MTPnullEntities,// #TODO l76 entities
+				MTPnullEntities, // #TODO caption entities
 				MTP_int(1),
 				MTPint(),
 				MTP_string(messagePostAuthor),
@@ -5143,7 +5141,7 @@ bool HistoryWidget::onStickerSend(DocumentData *sticker) {
 			return false;
 		}
 	}
-	return sendExistingDocument(sticker, QString());
+	return sendExistingDocument(sticker, TextWithEntities());
 }
 
 void HistoryWidget::onPhotoSend(PhotoData *photo) {
@@ -5155,7 +5153,7 @@ void HistoryWidget::onPhotoSend(PhotoData *photo) {
 			return;
 		}
 	}
-	sendExistingPhoto(photo, QString());
+	sendExistingPhoto(photo, TextWithEntities());
 }
 
 void HistoryWidget::onInlineResultSend(
@@ -5368,7 +5366,7 @@ void HistoryWidget::destroyPinnedBar() {
 
 bool HistoryWidget::sendExistingDocument(
 		DocumentData *doc,
-		const QString &caption) {
+		TextWithEntities caption) {
 	if (!_peer || !_peer->canWrite() || !doc) {
 		return false;
 	}
@@ -5409,6 +5407,15 @@ bool HistoryWidget::sendExistingDocument(
 	}
 	auto messageFromId = channelPost ? 0 : Auth().userId();
 	auto messagePostAuthor = channelPost ? (Auth().user()->firstName + ' ' + Auth().user()->lastName) : QString();
+
+	TextUtilities::Trim(caption);
+	auto sentEntities = TextUtilities::EntitiesToMTP(
+		caption.entities,
+		TextUtilities::ConvertOption::SkipLocal);
+	if (!sentEntities.v.isEmpty()) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
+	}
+
 	_history->addNewDocument(
 		newId.msg,
 		flags,
@@ -5420,7 +5427,6 @@ bool HistoryWidget::sendExistingDocument(
 		doc,
 		caption,
 		MTPnullMarkup);
-
 	_history->sendRequestId = MTP::send(
 		MTPmessages_SendMedia(
 			MTP_flags(sendFlags),
@@ -5430,10 +5436,10 @@ bool HistoryWidget::sendExistingDocument(
 				MTP_flags(0),
 				mtpInput,
 				MTPint()),
-			MTP_string(caption),
+			MTP_string(caption.text),
 			MTP_long(randomId),
 			MTPnullMarkup,
-			MTPnullEntities), // #TODO l76 entities
+			sentEntities),
 		App::main()->rpcDone(&MainWidget::sentUpdatesReceived),
 		App::main()->rpcFail(&MainWidget::sendMessageFail),
 		0,
@@ -5461,7 +5467,7 @@ bool HistoryWidget::sendExistingDocument(
 
 void HistoryWidget::sendExistingPhoto(
 		PhotoData *photo,
-		const QString &caption) {
+		TextWithEntities caption) {
 	if (!_peer || !_peer->canWrite() || !photo) {
 		return;
 	}
@@ -5497,6 +5503,15 @@ void HistoryWidget::sendExistingPhoto(
 	}
 	auto messageFromId = channelPost ? 0 : Auth().userId();
 	auto messagePostAuthor = channelPost ? (Auth().user()->firstName + ' ' + Auth().user()->lastName) : QString();
+
+	TextUtilities::Trim(caption);
+	auto sentEntities = TextUtilities::EntitiesToMTP(
+		caption.entities,
+		TextUtilities::ConvertOption::SkipLocal);
+	if (!sentEntities.v.isEmpty()) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
+	}
+
 	_history->addNewPhoto(
 		newId.msg,
 		flags,
@@ -5518,10 +5533,10 @@ void HistoryWidget::sendExistingPhoto(
 				MTP_flags(0),
 				MTP_inputPhoto(MTP_long(photo->id), MTP_long(photo->access)),
 				MTPint()),
-			MTP_string(caption),
+			MTP_string(caption.text),
 			MTP_long(randomId),
 			MTPnullMarkup,
-			MTPnullEntities), // #TODO l76 entities
+			sentEntities),
 		App::main()->rpcDone(&MainWidget::sentUpdatesReceived),
 		App::main()->rpcFail(&MainWidget::sendMessageFail),
 		0,
@@ -5627,7 +5642,7 @@ void HistoryWidget::editMessage(FullMsgId itemId) {
 void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
 	if (const auto media = item->media()) {
 		if (media->allowsEditCaption()) {
-			Ui::show(Box<EditCaptionBox>(media, item->fullId()));
+			Ui::show(Box<EditCaptionBox>(item));
 			return;
 		}
 	}
@@ -6391,7 +6406,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) {
 				} else {
 					_replyToName.drawElided(p, replyLeft, backy + st::msgReplyPadding.top(), width() - replyLeft - _fieldBarCancel->width() - st::msgReplyPadding.right());
 				}
-				p.setPen(((drawMsgText->toHistoryMessage() && drawMsgText->toHistoryMessage()->emptyText()) || drawMsgText->serviceMsg()) ? st::historyComposeAreaFgService : st::historyComposeAreaFg);
+				p.setPen(!drawMsgText->toHistoryMessage() ? st::historyComposeAreaFgService : st::historyComposeAreaFg);
 				_replyEditMsgText.drawElided(p, replyLeft, backy + st::msgReplyPadding.top() + st::msgServiceNameFont->height, width() - replyLeft - _fieldBarCancel->width() - st::msgReplyPadding.right());
 			} else {
 				p.setFont(st::msgDateFont);
@@ -6552,7 +6567,7 @@ void HistoryWidget::drawPinnedBar(Painter &p) {
 		p.setFont(st::msgServiceNameFont);
 		p.drawText(left, top + st::msgServiceNameFont->ascent, lang(lng_pinned_message));
 
-		p.setPen(((_pinnedBar->msg->toHistoryMessage() && _pinnedBar->msg->toHistoryMessage()->emptyText()) || _pinnedBar->msg->serviceMsg()) ? st::historyComposeAreaFgService : st::historyComposeAreaFg);
+		p.setPen(!_pinnedBar->msg->toHistoryMessage() ? st::historyComposeAreaFgService : st::historyComposeAreaFg);
 		_pinnedBar->text.drawElided(p, left, top + st::msgServiceNameFont->height, width() - left - _pinnedBar->cancel->width() - st::msgReplyPadding.right());
 	} else {
 		p.setFont(st::msgDateFont);
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index 9f5e160911..b7673db2af 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -593,8 +593,8 @@ private:
 	void destroyPinnedBar();
 	void unpinDone(const MTPUpdates &updates);
 
-	bool sendExistingDocument(DocumentData *doc, const QString &caption);
-	void sendExistingPhoto(PhotoData *photo, const QString &caption);
+	bool sendExistingDocument(DocumentData *doc, TextWithEntities caption);
+	void sendExistingPhoto(PhotoData *photo, TextWithEntities caption);
 
 	void drawField(Painter &p, const QRect &rect);
 	void paintEditHeader(Painter &p, const QRect &rect, int left, int top) const;
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index c8b761a6ad..57313e3700 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -470,6 +470,10 @@ QDateTime Element::displayedEditDate() const {
 	return QDateTime();
 }
 
+bool Element::hasVisibleText() const {
+	return false;
+}
+
 HistoryBlock *Element::block() {
 	return _block;
 }
@@ -495,6 +499,12 @@ void Element::removeFromBlock() {
 	_block->remove(this);
 }
 
+void Element::refreshInBlock() {
+	Expects(_block != nullptr);
+
+	_block->refreshView(this);
+}
+
 void Element::setIndexInBlock(int index) {
 	Expects(_block != nullptr);
 	Expects(index >= 0);
diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h
index 52282d33d9..169c578284 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.h
+++ b/Telegram/SourceFiles/history/view/history_view_element.h
@@ -173,12 +173,14 @@ public:
 	virtual ClickHandlerPtr rightActionLink() const;
 	virtual bool displayEditedBadge() const;
 	virtual QDateTime displayedEditDate() const;
+	virtual bool hasVisibleText() const;
 
 	// Legacy blocks structure.
 	HistoryBlock *block();
 	const HistoryBlock *block() const;
 	void attachToBlock(not_null<HistoryBlock*> block, int index);
 	void removeFromBlock();
+	void refreshInBlock();
 	void setIndexInBlock(int index);
 	int indexInBlock() const;
 	Element *previousInBlocks() const;
diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
index 016a4eae89..12ce5e8662 100644
--- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
@@ -874,7 +874,7 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 						}
 					}
 				}
-				if (msg && !_contextMenuLink && (!msg->emptyText() || mediaHasTextForCopy)) {
+				if (!_contextMenuLink && (view->hasVisibleText() || mediaHasTextForCopy)) {
 					_menu->addAction(lang(lng_context_copy_text), [=] {
 						copyContextText(itemId);
 					})->setEnabled(true);
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index 73000510b3..da773d572a 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -258,7 +258,7 @@ QSize Message::performCountOptimalSize() {
 		}
 
 		maxWidth = item->plainMaxWidth();
-		minHeight = item->emptyText() ? 0 : item->_text.minHeight();
+		minHeight = hasVisibleText() ? item->_text.minHeight() : 0;
 		if (!mediaOnBottom) {
 			minHeight += st::msgPadding.bottom();
 			if (mediaDisplayed) minHeight += st::mediaInBubbleSkip;
@@ -333,7 +333,7 @@ QSize Message::performCountOptimalSize() {
 
 		// if we have a text bubble we can resize it to fit the keyboard
 		// but if we have only media we don't do that
-		if (!item->emptyText()) {
+		if (hasVisibleText()) {
 			accumulate_max(maxWidth, markup->inlineKeyboard->naturalWidth());
 		}
 	}
@@ -1278,7 +1278,7 @@ bool Message::drawBubble() const {
 	}
 	const auto media = this->media();
 	return media
-		? (!item->emptyText() || media->needsBubble())
+		? (hasVisibleText() || media->needsBubble())
 		: !item->isEmpty();
 }
 
@@ -1401,7 +1401,7 @@ void Message::updateMediaInBubbleState() {
 		mediaHasSomethingBelow = true;
 		mediaHasSomethingAbove = getMediaHasSomethingAbove();
 		auto entryState = (mediaHasSomethingAbove
-			|| !item->emptyText()
+			|| hasVisibleText()
 			|| (media && media->isDisplayed()))
 			? MediaInBubbleState::Bottom
 			: MediaInBubbleState::None;
@@ -1420,7 +1420,7 @@ void Message::updateMediaInBubbleState() {
 	if (!entry) {
 		mediaHasSomethingAbove = getMediaHasSomethingAbove();
 	}
-	if (!item->emptyText()) {
+	if (hasVisibleText()) {
 		if (media->isAboveMessage()) {
 			mediaHasSomethingBelow = true;
 		} else {
@@ -1554,15 +1554,15 @@ int Message::resizeContentGetHeight(int newWidth) {
 				entry->resizeGetHeight(countGeometry().width());
 			}
 		} else {
-			if (item->emptyText()) {
-				newHeight = 0;
-			} else {
+			if (hasVisibleText()) {
 				auto textWidth = qMax(contentWidth - st::msgPadding.left() - st::msgPadding.right(), 1);
 				if (textWidth != item->_textWidth) {
 					item->_textWidth = textWidth;
 					item->_textHeight = item->_text.countHeight(textWidth);
 				}
 				newHeight = item->_textHeight;
+			} else {
+				newHeight = 0;
 			}
 			if (!mediaOnBottom) {
 				newHeight += st::msgPadding.bottom();
@@ -1616,6 +1616,14 @@ int Message::resizeContentGetHeight(int newWidth) {
 	return newHeight;
 }
 
+bool Message::hasVisibleText() const {
+	if (message()->emptyText()) {
+		return false;
+	}
+	const auto media = this->media();
+	return !media || !media->hideMessageText();
+}
+
 QSize Message::performCountCurrentSize(int newWidth) {
 	const auto item = message();
 	const auto newHeight = resizeContentGetHeight(newWidth);
diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h
index 6e21a2092c..f393a0f077 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.h
+++ b/Telegram/SourceFiles/history/view/history_view_message.h
@@ -128,6 +128,7 @@ private:
 	int resizeContentGetHeight(int newWidth);
 	QSize performCountOptimalSize() override;
 	QSize performCountCurrentSize(int newWidth) override;
+	bool hasVisibleText() const override;
 
 	bool displayFastShare() const;
 	bool displayGoToOriginal() const;
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
index af3b8de2fe..3e53ed29cd 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
@@ -135,8 +135,7 @@ void SendPhoto::addToHistory(
 		fromId,
 		postAuthor,
 		_photo,
-		_message,
-		//_entities,
+		{ _message, _entities },
 		markup);
 }
 
@@ -171,8 +170,7 @@ void SendFile::addToHistory(
 		fromId,
 		postAuthor,
 		_document,
-		_message,
-		//_entities,
+		{ _message, _entities },
 		markup);
 }
 
diff --git a/Telegram/SourceFiles/mediaview.cpp b/Telegram/SourceFiles/mediaview.cpp
index 6cb58d7812..78b1a46972 100644
--- a/Telegram/SourceFiles/mediaview.cpp
+++ b/Telegram/SourceFiles/mediaview.cpp
@@ -1237,14 +1237,11 @@ void MediaView::refreshMediaViewer() {
 
 void MediaView::refreshCaption(HistoryItem *item) {
 	_caption = Text();
-
-	const auto media = item ? item->media() : nullptr;
-	if (!media) {
+	if (!item) {
 		return;
 	}
-
-	const auto caption = media->caption();
-	if (caption.isEmpty()) {
+	const auto caption = item->originalText();
+	if (caption.text.isEmpty()) {
 		return;
 	}
 	const auto asBot = [&] {
@@ -1256,7 +1253,7 @@ void MediaView::refreshCaption(HistoryItem *item) {
 	_caption = Text(st::msgMinWidth);
 	_caption.setMarkedText(
 		st::mediaviewCaptionStyle,
-		{ caption, EntitiesInText() }, // #TODO caption entities parse
+		caption,
 		Ui::ItemTextOptions(item));
 }