From 45143c40c967f9dab1085959a37779f0861d725d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 4 May 2016 19:46:24 +0300
Subject: [PATCH] FlatTextarea handles tags on insertFromMime and tags editing.
 Fixed dependent messages update when message was edited. Fixed entities
 parsing in Text when they follow one after another.

---
 Telegram/SourceFiles/history.cpp         |  21 +-
 Telegram/SourceFiles/history.h           |   3 +-
 Telegram/SourceFiles/ui/emoji_config.h   |  26 +-
 Telegram/SourceFiles/ui/flattextarea.cpp | 340 ++++++++++++++++-------
 Telegram/SourceFiles/ui/flattextarea.h   |  18 +-
 Telegram/SourceFiles/ui/text/text.cpp    |  12 +-
 6 files changed, 293 insertions(+), 127 deletions(-)

diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp
index e04c081e3..7907cdc2d 100644
--- a/Telegram/SourceFiles/history.cpp
+++ b/Telegram/SourceFiles/history.cpp
@@ -6960,12 +6960,12 @@ void HistoryMessage::initDimensions() {
 			if (_media->isDisplayed()) {
 				if (_text.hasSkipBlock()) {
 					_text.removeSkipBlock();
-					_textWidth = 0;
+					_textWidth = -1;
 					_textHeight = 0;
 				}
 			} else if (!_text.hasSkipBlock()) {
 				_text.setSkipBlock(skipBlockWidth(), skipBlockHeight());
-				_textWidth = 0;
+				_textWidth = -1;
 				_textHeight = 0;
 			}
 		}
@@ -7103,6 +7103,8 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) {
 			keyboard->oldTop = keyboardTop;
 		}
 	}
+
+	App::historyUpdateDependent(this);
 }
 
 void HistoryMessage::updateMedia(const MTPMessageMedia *media) {
@@ -7211,11 +7213,11 @@ void HistoryMessage::setMedia(const MTPMessageMedia *media) {
 	initMedia(media, t);
 	if (_media && _media->isDisplayed() && !mediaWasDisplayed) {
 		_text.removeSkipBlock();
-		_textWidth = 0;
+		_textWidth = -1;
 		_textHeight = 0;
 	} else if (mediaWasDisplayed && (!_media || !_media->isDisplayed())) {
 		_text.setSkipBlock(skipBlockWidth(), skipBlockHeight());
-		_textWidth = 0;
+		_textWidth = -1;
 		_textHeight = 0;
 	}
 }
@@ -7236,7 +7238,7 @@ void HistoryMessage::setText(const QString &text, const EntitiesInText &entities
 			break;
 		}
 	}
-	_textWidth = 0;
+	_textWidth = -1;
 	_textHeight = 0;
 }
 
@@ -7395,7 +7397,7 @@ void HistoryMessage::setViewsCount(int32 count) {
 	} else {
 		if (_text.hasSkipBlock()) {
 			_text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight());
-			_textWidth = 0;
+			_textWidth = -1;
 			_textHeight = 0;
 		}
 		setPendingInitDimensions();
@@ -7410,7 +7412,7 @@ void HistoryMessage::setId(MsgId newId) {
 	} else {
 		if (_text.hasSkipBlock()) {
 			_text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight());
-			_textWidth = 0;
+			_textWidth = -1;
 			_textHeight = 0;
 		}
 		setPendingInitDimensions();
@@ -8079,7 +8081,6 @@ bool HistoryService::updatePinned(bool force) {
 		updatePinnedText();
 	}
 	if (force) {
-		setPendingInitDimensions();
 		if (gotDependencyItem && App::wnd()) {
 			App::wnd()->notifySettingGot();
 		}
@@ -8208,7 +8209,9 @@ void HistoryService::setServiceText(const QString &text) {
 	textstyleSet(&st::serviceTextStyle);
 	_text.setText(st::msgServiceFont, text, _historySrvOptions);
 	textstyleRestore();
-	initDimensions();
+	setPendingInitDimensions();
+	_textWidth = -1;
+	_textHeight = 0;
 }
 
 void HistoryService::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const {
diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h
index 9c850a53a..cc8a8bec1 100644
--- a/Telegram/SourceFiles/history.h
+++ b/Telegram/SourceFiles/history.h
@@ -1566,7 +1566,8 @@ protected:
 	}
 
 	Text _text = { int(st::msgMinWidth) };
-	int32 _textWidth, _textHeight;
+	int _textWidth = -1;
+	int _textHeight = 0;
 
 	HistoryMediaPtr _media;
 
diff --git a/Telegram/SourceFiles/ui/emoji_config.h b/Telegram/SourceFiles/ui/emoji_config.h
index ed7bae85e..955811f11 100644
--- a/Telegram/SourceFiles/ui/emoji_config.h
+++ b/Telegram/SourceFiles/ui/emoji_config.h
@@ -83,52 +83,52 @@ inline EmojiPtr emojiFromUrl(const QString &url) {
 	return emojiFromKey(url.midRef(10).toULongLong(0, 16)); // skip emoji://e.
 }
 
-inline EmojiPtr emojiFromText(const QChar *ch, const QChar *e, int *plen = 0) {
-	EmojiPtr emoji = 0;
-	if (ch + 1 < e && ((ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) || (((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0x20E3))) {
+inline EmojiPtr emojiFromText(const QChar *ch, const QChar *end, int *outLength = nullptr) {
+	EmojiPtr emoji = nullptr;
+	if (ch + 1 < end && ((ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) || (((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0x20E3))) {
 		uint32 code = (ch->unicode() << 16) | (ch + 1)->unicode();
 		emoji = emojiGet(code);
 		if (emoji) {
 			if (emoji == TwoSymbolEmoji) { // check two symbol
-				if (ch + 3 >= e) {
+				if (ch + 3 >= end) {
 					emoji = 0;
 				} else {
 					uint32 code2 = ((uint32((ch + 2)->unicode()) << 16) | uint32((ch + 3)->unicode()));
 					emoji = emojiGet(code, code2);
 				}
 			} else {
-				if (ch + 2 < e && (ch + 2)->unicode() == 0x200D) { // check sequence
-					EmojiPtr seq = emojiGet(ch, e);
+				if (ch + 2 < end && (ch + 2)->unicode() == 0x200D) { // check sequence
+					EmojiPtr seq = emojiGet(ch, end);
 					if (seq) {
 						emoji = seq;
 					}
 				}
 			}
 		}
-	} else if (ch + 2 < e && ((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0xFE0F && (ch + 2)->unicode() == 0x20E3) {
+	} else if (ch + 2 < end && ((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0xFE0F && (ch + 2)->unicode() == 0x20E3) {
 		uint32 code = (ch->unicode() << 16) | (ch + 2)->unicode();
 		emoji = emojiGet(code);
-		if (plen) *plen = emoji->len + 1;
+		if (outLength) *outLength = emoji->len + 1;
 		return emoji;
-	} else if (ch < e) {
+	} else if (ch < end) {
 		emoji = emojiGet(ch->unicode());
 		t_assert(emoji != TwoSymbolEmoji);
 	}
 
 	if (emoji) {
-		int32 len = emoji->len + ((ch + emoji->len < e && (ch + emoji->len)->unicode() == 0xFE0F) ? 1 : 0);
-		if (emoji->color && (ch + len + 1 < e && (ch + len)->isHighSurrogate() && (ch + len + 1)->isLowSurrogate())) { // color
+		int32 len = emoji->len + ((ch + emoji->len < end && (ch + emoji->len)->unicode() == 0xFE0F) ? 1 : 0);
+		if (emoji->color && (ch + len + 1 < end && (ch + len)->isHighSurrogate() && (ch + len + 1)->isLowSurrogate())) { // color
 			uint32 color = ((uint32((ch + len)->unicode()) << 16) | uint32((ch + len + 1)->unicode()));
 			EmojiPtr col = emojiGet(emoji, color);
 			if (col && col != emoji) {
 				len += col->len - emoji->len;
 				emoji = col;
-				if (ch + len < e && (ch + len)->unicode() == 0xFE0F) {
+				if (ch + len < end && (ch + len)->unicode() == 0xFE0F) {
 					++len;
 				}
 			}
 		}
-		if (plen) *plen = len;
+		if (outLength) *outLength = len;
 	}
 
 	return emoji;
diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp
index 45ef62746..51c7ee0f5 100644
--- a/Telegram/SourceFiles/ui/flattextarea.cpp
+++ b/Telegram/SourceFiles/ui/flattextarea.cpp
@@ -310,7 +310,7 @@ EmojiPtr FlatTextarea::getSingleEmoji() const {
 			return emojiFromUrl(imageName);
 		}
 	}
-	return 0;
+	return nullptr;
 }
 
 QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInlineBotUsername) const {
@@ -464,14 +464,16 @@ void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const Q
 		break;
 	}
 	if (tagId.isEmpty()) {
-		c.insertText(data + ' ');
+		QTextCharFormat format = c.charFormat();
+		format.setAnchor(false);
+		format.setAnchorName(QString());
+		format.clearForeground();
+		c.insertText(data + ' ', format);
 	} else {
-		QTextCharFormat defaultFormat = c.charFormat(), linkFormat = defaultFormat;
-		linkFormat.setAnchor(true);
-		linkFormat.setAnchorName(tagId + '/' + QString::number(rand_value<uint32>()));
-		linkFormat.setForeground(st::defaultTextStyle.linkFg);
-		c.insertText(data, linkFormat);
-		c.insertText(qsl(" "), defaultFormat);
+		_insertedTags.clear();
+		_insertedTags.push_back({ 0, data.size(), tagId + '/' + QString::number(rand_value<uint32>()) });
+		c.insertText(data + ' ');
+		_insertedTags.clear();
 	}
 }
 
@@ -792,16 +794,21 @@ QStringList FlatTextarea::linksList() const {
 
 void FlatTextarea::insertFromMimeData(const QMimeData *source) {
 	auto mime = str_const_toString(TagsMimeType);
+	auto text = source->text();
 	if (source->hasFormat(mime)) {
 		auto tagsData = source->data(mime);
-		_settingTags = deserializeTagsList(tagsData, source->text().size());
+		_insertedTags = deserializeTagsList(tagsData, text.size());
 	} else {
-		_settingTags.clear();
+		_insertedTags.clear();
 	}
+	_realInsertPosition = textCursor().position();
+	_realCharsAdded = text.size();
 	QTextEdit::insertFromMimeData(source);
-	_settingTags.clear();
-
-	if (!_inDrop) emit spacedReturnedPasted();
+	if (!_inDrop) {
+		emit spacedReturnedPasted();
+		_insertedTags.clear();
+		_realInsertPosition = -1;
+	}
 }
 
 void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) {
@@ -811,8 +818,11 @@ void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) {
 	imageFormat.setHeight(eh / cIntRetinaFactor());
 	imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16));
 	imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline);
-	imageFormat.setAnchorName(c.charFormat().anchorName());
-
+	if (c.charFormat().isAnchor()) {
+		imageFormat.setAnchor(true);
+		imageFormat.setAnchorName(c.charFormat().anchorName());
+		imageFormat.setForeground(st::defaultTextStyle.linkFg);
+	}
 	static QString objectReplacement(QChar::ObjectReplacementCharacter);
 	c.insertText(objectReplacement, imageFormat);
 }
@@ -833,92 +843,234 @@ void FlatTextarea::checkContentHeight() {
 	}
 }
 
-void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) {
-	int32 replacePosition = -1, replaceLen = 0;
-	const EmojiData *emoji = nullptr;
+namespace {
 
-	static QString regular = qsl("Open Sans"), semibold = qsl("Open Sans Semibold");
-	bool checkTilde = !cRetina() && (font().pixelSize() == 13) && (font().family() == regular), wasTildeFragment = false;
+// Optimization: with null page size document does not re-layout
+// on each insertText / mergeCharFormat.
+void prepareFormattingOptimization(QTextDocument *document) {
+	if (!document->pageSize().isNull()) {
+		document->setPageSize(QSizeF(0, 0));
+	}
+}
 
-	QTextDocument *doc(document());
+void removeTags(QTextDocument *document, int from, int end) {
+	QTextCursor c(document->docHandle(), from);
+	c.setPosition(end, QTextCursor::KeepAnchor);
+
+	QTextCharFormat format;
+	format.setAnchor(false);
+	format.setAnchorName(QString());
+	format.setForeground(st::black);
+	c.mergeCharFormat(format);
+}
+
+// Returns the position of the first inserted tag or "changedEnd" value if none found.
+int processInsertedTags(QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags) {
+	int firstTagStart = changedEnd;
+	int applyNoTagFrom = changedEnd;
+	for_const (auto &tag, tags) {
+		int tagFrom = changedPosition + tag.offset;
+		int tagTo = tagFrom + tag.length;
+		accumulate_max(tagFrom, changedPosition);
+		accumulate_min(tagTo, changedEnd);
+		if (tagTo > tagFrom) {
+			accumulate_min(firstTagStart, tagFrom);
+
+			prepareFormattingOptimization(document);
+
+			if (applyNoTagFrom < tagFrom) {
+				removeTags(document, applyNoTagFrom, tagFrom);
+			}
+			QTextCursor c(document->docHandle(), tagFrom);
+			c.setPosition(tagTo, QTextCursor::KeepAnchor);
+
+			QTextCharFormat format;
+			format.setAnchor(true);
+			format.setAnchorName(tag.id);
+			format.setForeground(st::defaultTextStyle.linkFg);
+			c.mergeCharFormat(format);
+
+			applyNoTagFrom = tagTo;
+		}
+	}
+	if (applyNoTagFrom < changedEnd) {
+		removeTags(document, applyNoTagFrom, changedEnd);
+	}
+
+	return firstTagStart;
+}
+
+// When inserting a part of text inside a tag we need to have
+// a way to know if the insertion replaced the end of the tag
+// or it was strictly inside (in the middle) of the tag.
+bool wasInsertTillTheEndOfTag(QTextBlock block, QTextBlock::iterator fragmentIt, int insertionEnd) {
+	auto insertTagName = fragmentIt.fragment().charFormat().anchorName();
 	while (true) {
-		int32 start = position, end = position + charsAdded;
-		QTextBlock from = doc->findBlock(start), till = doc->findBlock(end);
-		if (till.isValid()) till = till.next();
+		for (; !fragmentIt.atEnd(); ++fragmentIt) {
+			auto fragment = fragmentIt.fragment();
+			bool fragmentOutsideInsertion = (fragment.position() >= insertionEnd);
+			if (fragmentOutsideInsertion) {
+				return (fragment.charFormat().anchorName() != insertTagName);
+			}
+			int fragmentEnd = fragment.position() + fragment.length();
+			bool notFullFragmentInserted = (fragmentEnd > insertionEnd);
+			if (notFullFragmentInserted) {
+				return false;
+			}
+		}
+		if (block.isValid()) {
+			fragmentIt = block.begin();
+			block = block.next();
+		} else {
+			break;
+		}
+	}
+	// Insertion goes till the end of the text => not strictly inside a tag.
+	return true;
+}
 
-		for (QTextBlock b = from; b != till; b = b.next()) {
-			for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) {
-				QTextFragment fragment(iter.fragment());
-				if (!fragment.isValid()) continue;
+struct FormattingAction {
+	enum class Type {
+		Invalid,
+		InsertEmoji,
+		TildeFont,
+		RemoveTag,
+	};
+	Type type = Type::Invalid;
+	EmojiPtr emoji = nullptr;
+	bool isTilde = false;
+	int intervalStart = 0;
+	int intervalEnd = 0;
+};
 
-				int32 fp = fragment.position(), fe = fp + fragment.length();
-				if (fp >= end || fe <= start) {
+} // namespace
+
+void FlatTextarea::processFormatting(int changedPosition, int changedEnd) {
+	// Tilde formatting.
+	auto regularFont = qsl("Open Sans"), semiboldFont = qsl("Open Sans Semibold");
+	bool tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == regularFont);
+	bool isTildeFragment = false;
+
+	// First tag handling (the one we inserted text to).
+	bool startTagFound = false;
+	bool breakTagOnNotLetter = false;
+
+	auto doc = document();
+
+	// Apply inserted tags.
+	int breakTagOnNotLetterTill = processInsertedTags(doc, changedPosition, changedEnd, _insertedTags);
+	using ActionType = FormattingAction::Type;
+	while (true) {
+		FormattingAction action;
+
+		auto fromBlock = doc->findBlock(changedPosition);
+		auto tillBlock = doc->findBlock(changedEnd);
+		if (tillBlock.isValid()) tillBlock = tillBlock.next();
+
+		for (auto block = fromBlock; block != tillBlock; block = block.next()) {
+			for (auto fragmentIt = block.begin(); !fragmentIt.atEnd(); ++fragmentIt) {
+				auto fragment = fragmentIt.fragment();
+				t_assert(fragment.isValid());
+
+				int fragmentPosition = fragment.position();
+				if (changedPosition >= fragmentPosition + fragment.length()) {
 					continue;
 				}
-
-				if (checkTilde) {
-					wasTildeFragment = (fragment.charFormat().fontFamily() == semibold);
+				int changedPositionInFragment = changedPosition - fragmentPosition; // Can be negative.
+				int changedEndInFragment = changedEnd - fragmentPosition;
+				if (changedEndInFragment <= 0) {
+					break;
 				}
 
-				QString t(fragment.text());
-				const QChar *ch = t.constData(), *e = ch + t.size();
-				for (; ch != e; ++ch, ++fp) {
-					int emojiLen = 0;
-					emoji = emojiFromText(ch, e, &emojiLen);
-					if (emoji) {
-						if (replacePosition >= 0) {
-							emoji = 0; // replace tilde char format first
-						} else {
-							replacePosition = fp;
-							replaceLen = emojiLen;
+				auto charFormat = fragment.charFormat();
+				if (tildeFormatting) {
+					isTildeFragment = (charFormat.fontFamily() == semiboldFont);
+				}
+
+				auto fragmentText = fragment.text();
+				auto *textStart = fragmentText.constData();
+				auto *textEnd = textStart + fragmentText.size();
+
+				if (!startTagFound) {
+					startTagFound = true;
+					auto tagName = charFormat.anchorName();
+					if (!tagName.isEmpty()) {
+						breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, changedEnd);
+					}
+				}
+
+				auto *ch = textStart + qMax(changedPositionInFragment, 0);
+				for (; ch < textEnd; ++ch) {
+					int emojiLength = 0;
+					if (auto emoji = emojiFromText(ch, textEnd, &emojiLength)) {
+						// Replace emoji if no current action is prepared.
+						if (action.type == ActionType::Invalid) {
+							action.type = ActionType::InsertEmoji;
+							action.emoji = emoji;
+							action.intervalStart = fragmentPosition + (ch - textStart);
+							action.intervalEnd = action.intervalStart + emojiLength;
 						}
 						break;
 					}
 
-					if (checkTilde && fp >= position) { // tilde fix in OpenSans
+					if (breakTagOnNotLetter && !ch->isLetter()) {
+						// Remove tag name till the end if no current action is prepared.
+						if (action.type != ActionType::Invalid) {
+							break;
+						}
+						breakTagOnNotLetter = false;
+						if (fragmentPosition + (ch - textStart) < breakTagOnNotLetterTill) {
+							action.type = ActionType::RemoveTag;
+							action.intervalStart = fragmentPosition + (ch - textStart);
+							action.intervalEnd = breakTagOnNotLetterTill;
+							break;
+						}
+					}
+					if (tildeFormatting) { // Tilde symbol fix in OpenSans.
 						bool tilde = (ch->unicode() == '~');
-						if ((tilde && !wasTildeFragment) || (!tilde && wasTildeFragment)) {
-							if (replacePosition < 0) {
-								replacePosition = fp;
-								replaceLen = 1;
+						if ((tilde && !isTildeFragment) || (!tilde && isTildeFragment)) {
+							if (action.type == ActionType::Invalid) {
+								action.type = ActionType::TildeFont;
+								action.intervalStart = fragmentPosition + (ch - textStart);
+								action.intervalEnd = action.intervalStart + 1;
+								action.isTilde = tilde;
 							} else {
-								++replaceLen;
+								++action.intervalEnd;
 							}
-						} else if (replacePosition >= 0) {
+						} else if (action.type == ActionType::TildeFont) {
 							break;
 						}
 					}
 
-					if (ch + 1 < e && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) {
+					if (ch + 1 < textEnd && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) {
 						++ch;
-						++fp;
+						++fragmentPosition;
 					}
 				}
-				if (replacePosition >= 0) break;
+				if (action.type != ActionType::Invalid) break;
 			}
-			if (replacePosition >= 0) break;
+			if (action.type != ActionType::Invalid) break;
 		}
-		if (replacePosition >= 0) {
-			// Optimization: with null page size document does not re-layout
-			// on each insertText / mergeCharFormat.
-			if (!document()->pageSize().isNull()) {
-				document()->setPageSize(QSizeF(0, 0));
-			}
+		if (action.type != ActionType::Invalid) {
+			prepareFormattingOptimization(doc);
 
-			QTextCursor c(doc->docHandle(), replacePosition);
-			c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor);
-			if (emoji) {
-				insertEmoji(emoji, c);
-			} else {
+			QTextCursor c(doc->docHandle(), action.intervalStart);
+			c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor);
+			if (action.type == ActionType::InsertEmoji) {
+				insertEmoji(action.emoji, c);
+				changedPosition = action.intervalStart + 1;
+			} else if (action.type == ActionType::RemoveTag) {
 				QTextCharFormat format;
-				format.setFontFamily(wasTildeFragment ? regular : semibold);
+				format.setAnchor(false);
+				format.setAnchorName(QString());
+				format.setForeground(st::black);
 				c.mergeCharFormat(format);
+			} else if (action.type == ActionType::TildeFont) {
+				QTextCharFormat format;
+				format.setFontFamily(action.isTilde ? semiboldFont : regularFont);
+				c.mergeCharFormat(format);
+				changedPosition = action.intervalEnd;
 			}
-			charsAdded -= replacePosition + replaceLen - position;
-			position = replacePosition + (emoji ? 1 : replaceLen);
-
-			emoji = nullptr;
-			replacePosition = -1;
 		} else {
 			break;
 		}
@@ -928,6 +1080,9 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) {
 void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) {
 	if (_correcting) return;
 
+	int insertPosition = (_realInsertPosition >= 0) ? _realInsertPosition : position;
+	int insertLength = (_realInsertPosition >= 0) ? _realCharsAdded : charsAdded;
+
 	QTextCursor(document()->docHandle(), 0).joinPreviousEditBlock();
 
 	_correcting = true;
@@ -936,18 +1091,18 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int
 		c.movePosition(QTextCursor::End);
 		int32 fullSize = c.position(), toRemove = fullSize - _maxLength;
 		if (toRemove > 0) {
-			if (toRemove > charsAdded) {
-				if (charsAdded) {
-					c.setPosition(position);
-					c.setPosition((position + charsAdded), QTextCursor::KeepAnchor);
+			if (toRemove > insertLength) {
+				if (insertLength) {
+					c.setPosition(insertPosition);
+					c.setPosition((insertPosition + insertLength), QTextCursor::KeepAnchor);
 					c.removeSelectedText();
 				}
-				c.setPosition(fullSize - (toRemove - charsAdded));
+				c.setPosition(fullSize - (toRemove - insertLength));
 				c.setPosition(fullSize, QTextCursor::KeepAnchor);
 				c.removeSelectedText();
 			} else {
-				c.setPosition(position + (charsAdded - toRemove));
-				c.setPosition(position + charsAdded, QTextCursor::KeepAnchor);
+				c.setPosition(insertPosition + (insertLength - toRemove));
+				c.setPosition(insertPosition + insertLength, QTextCursor::KeepAnchor);
 				c.removeSelectedText();
 			}
 		}
@@ -957,10 +1112,10 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int
 	if (!_links.isEmpty()) {
 		bool changed = false;
 		for (auto i = _links.begin(); i != _links.end();) {
-			if (i->first + i->second <= position) {
+			if (i->first + i->second <= insertPosition) {
 				++i;
-			} else if (i->first >= position + charsRemoved) {
-				i->first += charsAdded - charsRemoved;
+			} else if (i->first >= insertPosition + charsRemoved) {
+				i->first += insertLength - charsRemoved;
 				++i;
 			} else {
 				i = _links.erase(i);
@@ -975,24 +1130,16 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int
 		return;
 	}
 
-	const int takeBack = 3;
-
-	position -= takeBack;
-	charsAdded += takeBack;
-	if (position < 0) {
-		charsAdded += position;
-		position = 0;
-	}
-	if (charsAdded <= 0) {
+	if (insertLength <= 0) {
 		QTextCursor(document()->docHandle(), 0).endEditBlock();
 		return;
 	}
 
 	_correcting = true;
-	QSizeF s = document()->pageSize();
-	processDocumentContentsChange(position, charsAdded);
-	if (document()->pageSize() != s) {
-		document()->setPageSize(s);
+	auto pageSize = document()->pageSize();
+	processFormatting(insertPosition, insertPosition + insertLength);
+	if (document()->pageSize() != pageSize) {
+		document()->setPageSize(pageSize);
 	}
 	_correcting = false;
 
@@ -1164,6 +1311,9 @@ void FlatTextarea::dropEvent(QDropEvent *e) {
 	_inDrop = true;
 	QTextEdit::dropEvent(e);
 	_inDrop = false;
+	_insertedTags.clear();
+	_realInsertPosition = -1;
+
 	emit spacedReturnedPasted();
 }
 
diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h
index 659d724e1..e9fe232da 100644
--- a/Telegram/SourceFiles/ui/flattextarea.h
+++ b/Telegram/SourceFiles/ui/flattextarea.h
@@ -143,7 +143,16 @@ private:
 	QString getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const;
 
 	void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const;
-	void processDocumentContentsChange(int position, int charsAdded);
+
+	// After any characters added we must postprocess them. This includes:
+	// 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px.
+	// 2. Replacing font family from semibold for all non-~ characters, if we used ...
+	// 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics.
+	// 4. Interrupting tags in which the text was inserted by any char except a letter.
+	// 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text.
+	// Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end).
+	void processFormatting(int changedPosition, int changedEnd);
+
 	bool heightAutoupdated();
 
 	int _minHeight = -1; // < 0 - no autosize
@@ -161,7 +170,12 @@ private:
 	Animation _a_appearance;
 
 	// Tags list which we should apply while setText() call or insert from mime data.
-	TagList _settingTags;
+	TagList _insertedTags;
+
+	// Override insert position and charsAdded from complex text editing
+	// (like drag-n-drop in the same text edit field).
+	int _realInsertPosition = -1;
+	int _realCharsAdded = 0;
 
 	style::flatTextarea _st;
 
diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp
index ed72e2a50..79b72b78f 100644
--- a/Telegram/SourceFiles/ui/text/text.cpp
+++ b/Telegram/SourceFiles/ui/text/text.cpp
@@ -253,7 +253,8 @@ public:
 		return result;
 	}
 
-	void checkEntities() {
+	// Returns true if at least one entity was parsed in the current position.
+	bool checkEntities() {
 		while (!removeFlags.isEmpty() && (ptr >= removeFlags.firstKey() || ptr >= end)) {
 			const QList<int32> &removing(removeFlags.first());
 			for (int32 i = removing.size(); i > 0;) {
@@ -272,7 +273,7 @@ public:
 			++waitingEntity;
 		}
 		if (waitingEntity == entitiesEnd || ptr < start + waitingEntity->offset()) {
-			return;
+			return false;
 		}
 
 		int32 startFlags = 0;
@@ -348,6 +349,7 @@ public:
 		} else {
 			while (waitingEntity != entitiesEnd && waitingEntity->length() <= 0) ++waitingEntity;
 		}
+		return true;
 	}
 
 	bool readSkipBlockCommand() {
@@ -620,11 +622,7 @@ public:
 		lastSkipped = false;
 		checkTilde = !cRetina() && _t->_font->size() == 13 && _t->_font->flags() == 0; // tilde Open Sans fix
 		for (; ptr <= end; ++ptr) {
-			checkEntities();
-			if (rich) {
-				if (checkCommand()) {
-					checkEntities();
-				}
+			while (checkEntities() || (rich && checkCommand())) {
 			}
 			parseCurrentChar();
 			parseEmojiFromCurrent();