From 463450e607a826a54e49714020fec747de9028c8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 5 May 2016 19:04:17 +0300
Subject: [PATCH] Saving FlatTextarea tags to drafts, applying them in setText.
 Now instead of plain text a TextWithTags struct is used almost everywhere.
 Started writing and reading serialized tags to drafts from 9048, switched
 version to 0.9.48 for testing.

---
 Telegram/Resources/winrc/Telegram.rc          |   8 +-
 Telegram/Resources/winrc/Updater.rc           |   8 +-
 Telegram/SourceFiles/core/basic_types.h       |  10 ++
 Telegram/SourceFiles/core/version.h           |   4 +-
 Telegram/SourceFiles/history.h                |  15 +-
 Telegram/SourceFiles/historywidget.cpp        | 158 +++++++++++-------
 Telegram/SourceFiles/historywidget.h          |  24 +--
 Telegram/SourceFiles/localstorage.cpp         |  66 +++++---
 Telegram/SourceFiles/localstorage.h           |   8 +-
 Telegram/SourceFiles/mainwidget.cpp           |  18 +-
 Telegram/SourceFiles/mainwidget.h             |   3 +-
 Telegram/SourceFiles/pspecific_mac.cpp        |   2 +-
 .../serialize/serialize_common.cpp            |   4 -
 .../SourceFiles/serialize/serialize_common.h  |  12 +-
 Telegram/SourceFiles/ui/flattextarea.cpp      | 116 ++++++++-----
 Telegram/SourceFiles/ui/flattextarea.h        |  63 ++++---
 Telegram/Telegram.xcodeproj/project.pbxproj   |   4 +-
 Telegram/build/version                        |   6 +-
 18 files changed, 324 insertions(+), 205 deletions(-)

diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 6352baeaf..6df5730f8 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -34,8 +34,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 0,9,47,0
- PRODUCTVERSION 0,9,47,0
+ FILEVERSION 0,9,48,0
+ PRODUCTVERSION 0,9,48,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -51,10 +51,10 @@ BEGIN
         BLOCK "040904b0"
         BEGIN
             VALUE "CompanyName", "Telegram Messenger LLP"
-            VALUE "FileVersion", "0.9.47.0"
+            VALUE "FileVersion", "0.9.48.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2016"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "0.9.47.0"
+            VALUE "ProductVersion", "0.9.48.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 182787008..5a58c3a01 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 0,9,47,0
- PRODUCTVERSION 0,9,47,0
+ FILEVERSION 0,9,48,0
+ PRODUCTVERSION 0,9,48,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -43,10 +43,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram Messenger LLP"
             VALUE "FileDescription", "Telegram Updater"
-            VALUE "FileVersion", "0.9.47.0"
+            VALUE "FileVersion", "0.9.48.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2016"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "0.9.47.0"
+            VALUE "ProductVersion", "0.9.48.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/basic_types.h b/Telegram/SourceFiles/core/basic_types.h
index 0e2d05e64..d828ffd20 100644
--- a/Telegram/SourceFiles/core/basic_types.h
+++ b/Telegram/SourceFiles/core/basic_types.h
@@ -166,6 +166,16 @@ template <typename T, size_t N> char(&ArraySizeHelper(T(&array)[N]))[N];
 #define qsl(s) QStringLiteral(s)
 #define qstr(s) QLatin1String(s, sizeof(s) - 1)
 
+// For QFlags<> declared in private section of a class we need to declare
+// operators from Q_DECLARE_OPERATORS_FOR_FLAGS as friend functions.
+#define Q_DECLARE_FRIEND_INCOMPATIBLE_FLAGS(Flags) \
+friend QIncompatibleFlag operator|(Flags::enum_type f1, int f2) Q_DECL_NOTHROW;
+
+#define Q_DECLARE_FRIEND_OPERATORS_FOR_FLAGS(Flags) \
+friend QFlags<Flags::enum_type> operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW; \
+friend QFlags<Flags::enum_type> operator|(Flags::enum_type f1, QFlags<Flags::enum_type> f2) Q_DECL_NOTHROW; \
+Q_DECLARE_FRIEND_INCOMPATIBLE_FLAGS(Flags)
+
 // using for_const instead of plain range-based for loop to ensure usage of const_iterator
 // it is important for the copy-on-write Qt containers
 // if you have "QVector<T*> v" then "for (T * const p : v)" will still call QVector::detach(),
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 4f4c0a710..cb1108299 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -24,7 +24,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 
 #define BETA_VERSION_MACRO (0ULL)
 
-constexpr int AppVersion = 9047;
-constexpr str_const AppVersionStr = "0.9.47";
+constexpr int AppVersion = 9048;
+constexpr str_const AppVersionStr = "0.9.48";
 constexpr bool AppAlphaVersion = true;
 constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO;
diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h
index cc8a8bec1..fd61a44bc 100644
--- a/Telegram/SourceFiles/history.h
+++ b/Telegram/SourceFiles/history.h
@@ -151,22 +151,23 @@ struct SendAction {
 	int32 progress;
 };
 
+using TextWithTags = FlatTextarea::TextWithTags;
 struct HistoryDraft {
 	HistoryDraft() : msgId(0), previewCancelled(false) {
 	}
-	HistoryDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled)
-		: text(text)
+	HistoryDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled)
+		: textWithTags(textWithTags)
 		, msgId(msgId)
 		, cursor(cursor)
 		, previewCancelled(previewCancelled) {
 	}
 	HistoryDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled)
-		: text(field.getLastText())
+		: textWithTags(field.getTextWithTags())
 		, msgId(msgId)
 		, cursor(field)
 		, previewCancelled(previewCancelled) {
 	}
-	QString text;
+	TextWithTags textWithTags;
 	MsgId msgId; // replyToId for message draft, editMsgId for edit draft
 	MessageCursor cursor;
 	bool previewCancelled;
@@ -176,8 +177,8 @@ struct HistoryEditDraft : public HistoryDraft {
 		: HistoryDraft()
 		, saveRequest(0) {
 	}
-	HistoryEditDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0)
-		: HistoryDraft(text, msgId, cursor, previewCancelled)
+	HistoryEditDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0)
+		: HistoryDraft(textWithTags, msgId, cursor, previewCancelled)
 		, saveRequest(saveRequest) {
 	}
 	HistoryEditDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequest = 0)
@@ -373,7 +374,7 @@ public:
 	}
 	void takeMsgDraft(History *from) {
 		if (auto &draft = from->_msgDraft) {
-			if (!draft->text.isEmpty() && !_msgDraft) {
+			if (!draft->textWithTags.text.isEmpty() && !_msgDraft) {
 				_msgDraft = std_::move(draft);
 				_msgDraft->msgId = 0; // edit and reply to drafts can't migrate
 			}
diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp
index 949c04817..448dbf54b 100644
--- a/Telegram/SourceFiles/historywidget.cpp
+++ b/Telegram/SourceFiles/historywidget.cpp
@@ -2053,8 +2053,8 @@ MessageField::MessageField(HistoryWidget *history, const style::flatTextarea &st
 }
 
 bool MessageField::hasSendText() const {
-	const QString &text(getLastText());
-	for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
+	auto &text(getTextWithTags().text);
+	for (auto *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
 		ushort code = ch->unicode();
 		if (code != ' ' && code != '\n' && code != '\r' && !chReplacedBySpace(code)) {
 			return true;
@@ -2735,7 +2735,7 @@ QPoint SilentToggle::tooltipPos() const {
 	return QCursor::pos();
 }
 
-EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) {
+EntitiesInText entitiesFromTextTags(const FlatTextarea::TagList &tags) {
 	EntitiesInText result;
 	if (tags.isEmpty()) {
 		return result;
@@ -2754,6 +2754,24 @@ EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) {
 	return result;
 }
 
+TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities) {
+	TextWithTags::Tags result;
+	if (entities.isEmpty()) {
+		return result;
+	}
+
+	result.reserve(entities.size());
+	for_const (auto &entity, entities) {
+		if (entity.type() == EntityInTextMentionName) {
+			auto match = QRegularExpression("^(\\d+\\.\\d+)$").match(entity.data());
+			if (match.hasMatch()) {
+				result.push_back({ entity.offset(), entity.length(), qstr("mention://user.") + entity.data() });
+			}
+		}
+	}
+	return result;
+}
+
 namespace {
 
 // For mention tags save and validate userId, ignore tags for different userId.
@@ -2974,7 +2992,7 @@ void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::
 	// Send bot command at once, if it was not inserted by pressing Tab.
 	if (str.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) {
 		App::sendBotCommand(_peer, nullptr, str);
-		setFieldText(_field.getLastText().mid(_field.textCursor().position()));
+		setFieldText(_field.getTextWithTagsPart(_field.textCursor().position()));
 	} else {
 		_field.insertTag(str);
 	}
@@ -3022,15 +3040,16 @@ void HistoryWidget::updateInlineBotQuery() {
 
 void HistoryWidget::updateStickersByEmoji() {
 	int32 len = 0;
-	if (EmojiPtr emoji = emojiFromText(_field.getLastText(), &len)) {
-		if (_field.getLastText().size() <= len) {
-			_fieldAutocomplete->showStickers(emoji);
-		} else {
+	auto &text = _field.getTextWithTags().text;
+	if (EmojiPtr emoji = emojiFromText(text, &len)) {
+		if (text.size() > len) {
 			len = 0;
+		} else {
+			_fieldAutocomplete->showStickers(emoji);
 		}
 	}
 	if (!len) {
-		_fieldAutocomplete->showStickers(EmojiPtr(0));
+		_fieldAutocomplete->showStickers(nullptr);
 	}
 }
 
@@ -3039,7 +3058,7 @@ void HistoryWidget::onTextChange() {
 	updateStickersByEmoji();
 
 	if (_peer && (!_peer->isChannel() || _peer->isMegagroup() || !_peer->asChannel()->canPublish() || (!_peer->asChannel()->isBroadcast() && !_broadcast.checked()))) {
-		if (!_inlineBot && !_editMsgId && (_textUpdateEventsFlags & TextUpdateEventsSendTyping)) {
+		if (!_inlineBot && !_editMsgId && (_textUpdateEvents.testFlag(TextUpdateEvent::SendTyping))) {
 			updateSendAction(_history, SendActionTyping);
 		}
 	}
@@ -3065,13 +3084,13 @@ void HistoryWidget::onTextChange() {
 		update();
 	}
 
-	if (!_peer || !(_textUpdateEventsFlags & TextUpdateEventsSaveDraft)) return;
+	if (!_peer || !(_textUpdateEvents.testFlag(TextUpdateEvent::SaveDraft))) return;
 	_saveDraftText = true;
 	onDraftSave(true);
 }
 
 void HistoryWidget::onDraftSaveDelayed() {
-	if (!_peer || !(_textUpdateEventsFlags & TextUpdateEventsSaveDraft)) return;
+	if (!_peer || !(_textUpdateEvents.testFlag(TextUpdateEvent::SaveDraft))) return;
 	if (!_field.textCursor().anchor() && !_field.textCursor().position() && !_field.verticalScrollBar()->value()) {
 		if (!Local::hasDraftCursors(_peer->id)) {
 			return;
@@ -3106,17 +3125,17 @@ void HistoryWidget::writeDrafts(HistoryDraft **msgDraft, HistoryEditDraft **edit
 			Local::MessageDraft localMsgDraft, localEditDraft;
 			if (msgDraft) {
 				if (*msgDraft) {
-					localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->text, (*msgDraft)->previewCancelled);
+					localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->textWithTags, (*msgDraft)->previewCancelled);
 				}
 			} else {
-				localMsgDraft = Local::MessageDraft(_replyToId, _field.getLastText(), _previewCancelled);
+				localMsgDraft = Local::MessageDraft(_replyToId, _field.getTextWithTags(), _previewCancelled);
 			}
 			if (editDraft) {
 				if (*editDraft) {
-					localEditDraft = Local::MessageDraft((*editDraft)->msgId, (*editDraft)->text, (*editDraft)->previewCancelled);
+					localEditDraft = Local::MessageDraft((*editDraft)->msgId, (*editDraft)->textWithTags, (*editDraft)->previewCancelled);
 				}
 			} else if (_editMsgId) {
-				localEditDraft = Local::MessageDraft(_editMsgId, _field.getLastText(), _previewCancelled);
+				localEditDraft = Local::MessageDraft(_editMsgId, _field.getTextWithTags(), _previewCancelled);
 			}
 			Local::writeDrafts(_peer->id, localMsgDraft, localEditDraft);
 			if (_migrated) {
@@ -3152,11 +3171,11 @@ void HistoryWidget::writeDrafts(History *history) {
 	Local::MessageDraft localMsgDraft, localEditDraft;
 	MessageCursor msgCursor, editCursor;
 	if (auto msgDraft = history->msgDraft()) {
-		localMsgDraft = Local::MessageDraft(msgDraft->msgId, msgDraft->text, msgDraft->previewCancelled);
+		localMsgDraft = Local::MessageDraft(msgDraft->msgId, msgDraft->textWithTags, msgDraft->previewCancelled);
 		msgCursor = msgDraft->cursor;
 	}
 	if (auto editDraft = history->editDraft()) {
-		localEditDraft = Local::MessageDraft(editDraft->msgId, editDraft->text, editDraft->previewCancelled);
+		localEditDraft = Local::MessageDraft(editDraft->msgId, editDraft->textWithTags, editDraft->previewCancelled);
 		editCursor = editDraft->cursor;
 	}
 	Local::writeDrafts(history->peer->id, localMsgDraft, localEditDraft);
@@ -3331,8 +3350,9 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query) {
 		}
 		bot->botInfo->inlineReturnPeerId = 0;
 		History *h = App::history(toPeerId);
-		auto text = '@' + bot->username + ' ' + query;
-		h->setMsgDraft(std_::make_unique<HistoryDraft>(text, 0, MessageCursor(text.size(), text.size(), QFIXED_MAX), false));
+		TextWithTags textWithTags = { '@' + bot->username + ' ' + query, TextWithTags::Tags() };
+		MessageCursor cursor = { textWithTags.text.size(), textWithTags.text.size(), QFIXED_MAX };
+		h->setMsgDraft(std_::make_unique<HistoryDraft>(textWithTags, 0, cursor, false));
 		if (h == _history) {
 			applyDraft();
 		} else {
@@ -3633,11 +3653,11 @@ void HistoryWidget::applyDraft(bool parseLinks) {
 		return;
 	}
 
-	_textUpdateEventsFlags = 0;
-	setFieldText(draft->text);
+	_textUpdateEvents = 0;
+	setFieldText(draft->textWithTags);
 	_field.setFocus();
 	draft->cursor.applyTo(_field);
-	_textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping;
+	_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
 	_previewCancelled = draft->previewCancelled;
 	if (auto editDraft = _history->editDraft()) {
 		_editMsgId = editDraft->msgId;
@@ -3730,7 +3750,7 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re
 		if (_editMsgId) {
 			_history->setEditDraft(std_::make_unique<HistoryEditDraft>(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId));
 		} else {
-			if (_replyToId || !_field.getLastText().isEmpty()) {
+			if (_replyToId || !_field.isEmpty()) {
 				_history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled));
 			} else {
 				_history->clearMsgDraft();
@@ -4738,11 +4758,11 @@ void HistoryWidget::preloadHistoryIfNeeded() {
 }
 
 void HistoryWidget::onInlineBotCancel() {
-	QString text = _field.getLastText();
-	if (text.size() > _inlineBotUsername.size() + 2) {
-		setFieldText('@' + _inlineBotUsername + ' ', TextUpdateEventsSaveDraft, false);
+	auto &textWithTags = _field.getTextWithTags();
+	if (textWithTags.text.size() > _inlineBotUsername.size() + 2) {
+		setFieldText({ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
 	} else {
-		clearFieldText(TextUpdateEventsSaveDraft, false);
+		clearFieldText(TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
 	}
 }
 
@@ -4783,11 +4803,10 @@ void HistoryWidget::saveEditMsg() {
 
 	WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0);
 
-	auto fieldText = _field.getLastText();
-	auto fieldTags = _field.getLastTags();
+	auto &textWithTags = _field.getTextWithTags();
 	auto prepareFlags = itemTextOptions(_history, App::self()).flags;
-	EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(fieldTags);
-	QString sendingText, leftText = prepareTextWithEntities(fieldText, prepareFlags, &leftEntities);
+	EntitiesInText sendingEntities, leftEntities = entitiesFromTextTags(textWithTags.tags);
+	QString sendingText, leftText = prepareTextWithEntities(textWithTags.text, prepareFlags, &leftEntities);
 
 	if (!textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) {
 		_field.selectAll();
@@ -4865,8 +4884,7 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) {
 
 	MainWidget::MessageToSend message;
 	message.history = _history;
-	message.text = _field.getLastText();
-	message.entities = _field.getLastTags();
+	message.textWithTags = _field.getTextWithTags();
 	message.replyTo = replyTo;
 	message.broadcast = _broadcast.checked();
 	message.silent = _silent.checked();
@@ -5379,7 +5397,7 @@ void HistoryWidget::sendBotCommand(PeerData *peer, UserData *bot, const QString
 
 	MainWidget::MessageToSend message;
 	message.history = _history;
-	message.text = toSend;
+	message.textWithTags = { toSend, TextWithTags::Tags() };
 	message.replyTo = replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0;
 	message.broadcast = false;
 	message.silent = false;
@@ -5460,8 +5478,9 @@ bool HistoryWidget::botCallbackFail(BotCallbackInfo info, const RPCError &error,
 bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
 	if (!_history) return false;
 
+	bool insertingInlineBot = !cmd.isEmpty() && (cmd.at(0) == '@');
 	QString toInsert = cmd;
-	if (!toInsert.isEmpty() && toInsert.at(0) != '@') {
+	if (!toInsert.isEmpty() && !insertingInlineBot) {
 		PeerData *bot = _peer->isUser() ? _peer : (App::hoveredLinkItem() ? App::hoveredLinkItem()->fromOriginal() : 0);
 		if (!bot->isUser() || !bot->asUser()->botInfo) bot = 0;
 		QString username = bot ? bot->asUser()->username : QString();
@@ -5472,28 +5491,33 @@ bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
 	}
 	toInsert += ' ';
 
-	if (toInsert.at(0) != '@') {
-		QString text = _field.getLastText();
+	if (!insertingInlineBot) {
+		auto &textWithTags = _field.getTextWithTags();
 		if (specialGif) {
-			if (text.trimmed() == '@' + cInlineGifBotUsername() && text.at(0) == '@') {
-				clearFieldText(TextUpdateEventsSaveDraft, false);
+			if (textWithTags.text.trimmed() == '@' + cInlineGifBotUsername() && textWithTags.text.at(0) == '@') {
+				clearFieldText(TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
 			}
 		} else {
-			QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(text);
+			TextWithTags textWithTagsToSet;
+			QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(textWithTags.text);
 			if (m.hasMatch()) {
-				text = toInsert + text.mid(m.capturedLength());
+				textWithTagsToSet = _field.getTextWithTagsPart(m.capturedLength());
 			} else {
-				text = toInsert + text;
+				textWithTagsToSet = textWithTags;
 			}
-			_field.setTextFast(text);
+			textWithTagsToSet.text = toInsert + textWithTagsToSet.text;
+			for (auto &tag : textWithTagsToSet.tags) {
+				tag.offset += toInsert.size();
+			}
+			_field.setTextWithTags(textWithTagsToSet);
 
 			QTextCursor cur(_field.textCursor());
 			cur.movePosition(QTextCursor::End);
 			_field.setTextCursor(cur);
 		}
 	} else {
-		if (!specialGif || _field.getLastText().isEmpty()) {
-			setFieldText(toInsert, TextUpdateEventsSaveDraft, false);
+		if (!specialGif || _field.isEmpty()) {
+			setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
 			_field.setFocus();
 			return true;
 		}
@@ -5787,7 +5811,7 @@ void HistoryWidget::onKbToggle(bool manual) {
 }
 
 void HistoryWidget::onCmdStart() {
-	setFieldText(qsl("/"));
+	setFieldText({ qsl("/"), TextWithTags::Tags() }, 0, FlatTextarea::AddToUndoHistory);
 }
 
 void HistoryWidget::contextMenuEvent(QContextMenuEvent *e) {
@@ -6998,7 +7022,7 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) {
 	} else if (e->key() == Qt::Key_Up) {
 		if (!(e->modifiers() & (Qt::ShiftModifier | Qt::MetaModifier | Qt::ControlModifier))) {
 			if (_history && _history->lastSentMsg && _history->lastSentMsg->canEdit(::date(unixtime()))) {
-				if (_field.getLastText().isEmpty() && !_editMsgId && !_replyToId) {
+				if (_field.isEmpty() && !_editMsgId && !_replyToId) {
 					App::contextItem(_history->lastSentMsg);
 					onEditMessage();
 				}
@@ -7308,11 +7332,11 @@ void HistoryWidget::sendExistingPhoto(PhotoData *photo, const QString &caption)
 	_field.setFocus();
 }
 
-void HistoryWidget::setFieldText(const QString &text, int32 textUpdateEventsFlags, bool clearUndoHistory) {
-	_textUpdateEventsFlags = textUpdateEventsFlags;
-	_field.setTextFast(text, clearUndoHistory);
+void HistoryWidget::setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events, FlatTextarea::UndoHistoryAction undoHistoryAction) {
+	_textUpdateEvents = events;
+	_field.setTextWithTags(textWithTags, undoHistoryAction);
 	_field.moveCursor(QTextCursor::End);
-	_textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping;
+	_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
 
 	_previewCancelled = false;
 	_previewData = nullptr;
@@ -7351,7 +7375,7 @@ void HistoryWidget::onReplyToMessage() {
 		if (auto msgDraft = _history->msgDraft()) {
 			msgDraft->msgId = to->id;
 		} else {
-			_history->setMsgDraft(std_::make_unique<HistoryDraft>(QString(), to->id, MessageCursor(), false));
+			_history->setMsgDraft(std_::make_unique<HistoryDraft>(TextWithTags(), to->id, MessageCursor(), false));
 		}
 	} else {
 		_replyEditMsg = to;
@@ -7384,14 +7408,17 @@ void HistoryWidget::onEditMessage() {
 	} else {
 		delete box;
 
-		if (_replyToId || !_field.getLastText().isEmpty()) {
+		if (_replyToId || !_field.isEmpty()) {
 			_history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled));
 		} else {
 			_history->clearMsgDraft();
 		}
 
-		QString text(textApplyEntities(to->originalText(), to->originalEntities()));
-		_history->setEditDraft(std_::make_unique<HistoryEditDraft>(text, to->id, MessageCursor(text.size(), text.size(), QFIXED_MAX), false));
+		auto originalText = to->originalText();
+		auto originalEntities = to->originalEntities();
+		TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) };
+		MessageCursor cursor = { original.text.size(), original.text.size(), QFIXED_MAX };
+		_history->setEditDraft(std_::make_unique<HistoryEditDraft>(original, to->id, cursor, false));
 		applyDraft(false);
 
 		_previewData = 0;
@@ -7510,7 +7537,7 @@ void HistoryWidget::cancelReply(bool lastKeyboardUsed) {
 		update();
 	} else if (auto msgDraft = (_history ? _history->msgDraft() : nullptr)) {
 		if (msgDraft->msgId) {
-			if (msgDraft->text.isEmpty()) {
+			if (msgDraft->textWithTags.text.isEmpty()) {
 				_history->clearMsgDraft();
 			} else {
 				msgDraft->msgId = 0;
@@ -7533,7 +7560,7 @@ void HistoryWidget::cancelEdit() {
 	if (!_editMsgId) return;
 
 	_editMsgId = 0;
-	_replyEditMsg = 0;
+	_replyEditMsg = nullptr;
 	_history->clearEditDraft();
 	applyDraft();
 
@@ -7546,21 +7573,21 @@ void HistoryWidget::cancelEdit() {
 	_saveDraftStart = getms();
 	onDraftSave();
 
-	mouseMoveEvent(0);
+	mouseMoveEvent(nullptr);
 	if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !replyToId()) {
 		_fieldBarCancel.hide();
 		updateMouseTracking();
 	}
 
-	int32 old = _textUpdateEventsFlags;
-	_textUpdateEventsFlags = 0;
+	auto old = _textUpdateEvents;
+	_textUpdateEvents = 0;
 	onTextChange();
-	_textUpdateEventsFlags = old;
+	_textUpdateEvents = old;
 
 	updateBotKeyboard();
 	updateFieldPlaceholder();
 
-	resizeEvent(0);
+	resizeEvent(nullptr);
 	update();
 }
 
@@ -7728,7 +7755,10 @@ void HistoryWidget::onCancel() {
 	if (_inlineBotCancel) {
 		onInlineBotCancel();
 	} else if (_editMsgId) {
-		if (_replyEditMsg && textApplyEntities(_replyEditMsg->originalText(), _replyEditMsg->originalEntities()) != _field.getLastText()) {
+		auto originalText = _replyEditMsg ? _replyEditMsg->originalText() : QString();
+		auto originalEntities = _replyEditMsg ? _replyEditMsg->originalEntities() : EntitiesInText();
+		TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) };
+		if (_replyEditMsg && original != _field.getTextWithTags()) {
 			auto box = new ConfirmBox(lang(lng_cancel_edit_post_sure), lang(lng_cancel_edit_post_yes), st::defaultBoxButton, lang(lng_cancel_edit_post_no));
 			connect(box, SIGNAL(confirmed()), this, SLOT(onFieldBarCancel()));
 			Ui::showLayer(box);
diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h
index 189afb9e4..76093067c 100644
--- a/Telegram/SourceFiles/historywidget.h
+++ b/Telegram/SourceFiles/historywidget.h
@@ -486,12 +486,8 @@ public:
 
 };
 
-EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags);
-
-enum TextUpdateEventsFlags {
-	TextUpdateEventsSaveDraft  = 0x01,
-	TextUpdateEventsSendTyping = 0x02,
-};
+EntitiesInText entitiesFromTextTags(const TextWithTags::Tags &tags);
+TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities);
 
 class HistoryWidget : public TWidget, public RPCSender {
 	Q_OBJECT
@@ -954,11 +950,18 @@ private:
 	void savedGifsGot(const MTPmessages_SavedGifs &gifs);
 	bool savedGifsFailed(const RPCError &error);
 
+	enum class TextUpdateEvent {
+		SaveDraft  = 0x01,
+		SendTyping = 0x02,
+	};
+	Q_DECLARE_FLAGS(TextUpdateEvents, TextUpdateEvent);
+	Q_DECLARE_FRIEND_OPERATORS_FOR_FLAGS(TextUpdateEvents);
+
 	void writeDrafts(HistoryDraft **msgDraft, HistoryEditDraft **editDraft);
 	void writeDrafts(History *history);
-	void setFieldText(const QString &text, int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true);
-	void clearFieldText(int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true) {
-		setFieldText(QString());
+	void setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory);
+	void clearFieldText(TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory) {
+		setFieldText(TextWithTags(), events, undoHistoryAction);
 	}
 
 	QStringList getMediasFromMime(const QMimeData *d);
@@ -1062,7 +1065,7 @@ private:
 	int32 _selCount; // < 0 - text selected, focus list, not _field
 
 	TaskQueue _fileLoader;
-	int32 _textUpdateEventsFlags = (TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping);
+	TextUpdateEvents _textUpdateEvents = (TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping);
 
 	int64 _serviceImageCacheSize = 0;
 	QString _confirmSource;
@@ -1095,3 +1098,4 @@ private:
 
 };
 
+Q_DECLARE_OPERATORS_FOR_FLAGS(HistoryWidget::TextUpdateEvents)
diff --git a/Telegram/SourceFiles/localstorage.cpp b/Telegram/SourceFiles/localstorage.cpp
index 1aee7a285..8df89e4c0 100644
--- a/Telegram/SourceFiles/localstorage.cpp
+++ b/Telegram/SourceFiles/localstorage.cpp
@@ -122,14 +122,6 @@ namespace {
 		return true;
 	}
 
-	uint32 _dateTimeSize() {
-		return (sizeof(qint64) + sizeof(quint32) + sizeof(qint8));
-	}
-
-	uint32 _bytearraySize(const QByteArray &arr) {
-		return sizeof(quint32) + arr.size();
-	}
-
 	QByteArray _settingsSalt, _passKeySalt, _passKeyEncrypted;
 
 	MTP::AuthKey _oldKey, _settingsKey, _passKey, _localKey;
@@ -628,18 +620,18 @@ namespace {
 				size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(i.value().name());
 				if (AppVersion > 9013) {
 					// bookmark
-					size += _bytearraySize(i.value().bookmark());
+					size += Serialize::bytearraySize(i.value().bookmark());
 				}
 				// date + size
-				size += _dateTimeSize() + sizeof(quint32);
+				size += Serialize::dateTimeSize() + sizeof(quint32);
 			}
 
 			//end mark
 			size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(QString());
 			if (AppVersion > 9013) {
-				size += _bytearraySize(QByteArray());
+				size += Serialize::bytearraySize(QByteArray());
 			}
-			size += _dateTimeSize() + sizeof(quint32);
+			size += Serialize::dateTimeSize() + sizeof(quint32);
 
 			size += sizeof(quint32); // aliases count
 			for (FileLocationAliases::const_iterator i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) {
@@ -1530,7 +1522,7 @@ namespace {
 		}
 
 		uint32 size = 16 * (sizeof(quint32) + sizeof(qint32));
-		size += sizeof(quint32) + Serialize::stringSize(cAskDownloadPath() ? QString() : cDownloadPath()) + _bytearraySize(cAskDownloadPath() ? QByteArray() : cDownloadPathBookmark());
+		size += sizeof(quint32) + Serialize::stringSize(cAskDownloadPath() ? QString() : cDownloadPath()) + Serialize::bytearraySize(cAskDownloadPath() ? QByteArray() : cDownloadPathBookmark());
 		size += sizeof(quint32) + sizeof(qint32) + (cRecentEmojisPreload().isEmpty() ? cGetRecentEmojis().size() : cRecentEmojisPreload().size()) * (sizeof(uint64) + sizeof(ushort));
 		size += sizeof(quint32) + sizeof(qint32) + cEmojiVariants().size() * (sizeof(uint32) + sizeof(uint64));
 		size += sizeof(quint32) + sizeof(qint32) + (cRecentStickersPreload().isEmpty() ? cGetRecentStickers().size() : cRecentStickersPreload().size()) * (sizeof(uint64) + sizeof(ushort));
@@ -2281,8 +2273,8 @@ namespace Local {
 	void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft) {
 		if (!_working()) return;
 
-		if (msgDraft.msgId <= 0 && msgDraft.text.isEmpty() && editDraft.msgId <= 0) {
-			DraftsMap::iterator i = _draftsMap.find(peer);
+		if (msgDraft.msgId <= 0 && msgDraft.textWithTags.text.isEmpty() && editDraft.msgId <= 0) {
+			auto i = _draftsMap.find(peer);
 			if (i != _draftsMap.cend()) {
 				clearKey(i.value());
 				_draftsMap.erase(i);
@@ -2292,17 +2284,26 @@ namespace Local {
 
 			_draftsNotReadMap.remove(peer);
 		} else {
-			DraftsMap::const_iterator i = _draftsMap.constFind(peer);
+			auto i = _draftsMap.constFind(peer);
 			if (i == _draftsMap.cend()) {
 				i = _draftsMap.insert(peer, genKey());
 				_mapChanged = true;
 				_writeMap(WriteMapFast);
 			}
 
-			EncryptedDescriptor data(sizeof(quint64) + Serialize::stringSize(msgDraft.text) + 2 * sizeof(qint32) + Serialize::stringSize(editDraft.text) + 2 * sizeof(qint32));
+			auto msgTags = FlatTextarea::serializeTagsList(msgDraft.textWithTags.tags);
+			auto editTags = FlatTextarea::serializeTagsList(editDraft.textWithTags.tags);
+
+			int size = sizeof(quint64);
+			size += Serialize::stringSize(msgDraft.textWithTags.text) + Serialize::bytearraySize(msgTags) + 2 * sizeof(qint32);
+			size += Serialize::stringSize(editDraft.textWithTags.text) + Serialize::bytearraySize(editTags) + 2 * sizeof(qint32);
+
+			EncryptedDescriptor data(size);
 			data.stream << quint64(peer);
-			data.stream << msgDraft.text << qint32(msgDraft.msgId) << qint32(msgDraft.previewCancelled ? 1 : 0);
-			data.stream << editDraft.text << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0);
+			data.stream << msgDraft.textWithTags.text << msgTags;
+			data.stream << qint32(msgDraft.msgId) << qint32(msgDraft.previewCancelled ? 1 : 0);
+			data.stream << editDraft.textWithTags.text << editTags;
+			data.stream << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0);
 
 			FileWriteDescriptor file(i.value());
 			file.writeEncrypted(data);
@@ -2370,15 +2371,23 @@ namespace Local {
 		}
 
 		quint64 draftPeer = 0;
-		QString msgText, editText;
+		TextWithTags msgData, editData;
+		QByteArray msgTagsSerialized, editTagsSerialized;
 		qint32 msgReplyTo = 0, msgPreviewCancelled = 0, editMsgId = 0, editPreviewCancelled = 0;
-		draft.stream >> draftPeer >> msgText;
+		draft.stream >> draftPeer >> msgData.text;
+		if (draft.version >= 9048) {
+			draft.stream >> msgTagsSerialized;
+		}
 		if (draft.version >= 7021) {
 			draft.stream >> msgReplyTo;
 			if (draft.version >= 8001) {
 				draft.stream >> msgPreviewCancelled;
 				if (!draft.stream.atEnd()) {
-					draft.stream >> editText >> editMsgId >> editPreviewCancelled;
+					draft.stream >> editData.text;
+					if (draft.version >= 9048) {
+						draft.stream >> editTagsSerialized;
+					}
+					draft.stream >> editMsgId >> editPreviewCancelled;
 				}
 			}
 		}
@@ -2389,18 +2398,21 @@ namespace Local {
 			return;
 		}
 
+		msgData.tags = FlatTextarea::deserializeTagsList(msgTagsSerialized, msgData.text.size());
+		editData.tags = FlatTextarea::deserializeTagsList(editTagsSerialized, editData.text.size());
+
 		MessageCursor msgCursor, editCursor;
 		_readDraftCursors(peer, msgCursor, editCursor);
 
-		if (msgText.isEmpty() && !msgReplyTo) {
+		if (msgData.text.isEmpty() && !msgReplyTo) {
 			h->clearMsgDraft();
 		} else {
-			h->setMsgDraft(std_::make_unique<HistoryDraft>(msgText, msgReplyTo, msgCursor, msgPreviewCancelled));
+			h->setMsgDraft(std_::make_unique<HistoryDraft>(msgData, msgReplyTo, msgCursor, msgPreviewCancelled));
 		}
 		if (!editMsgId) {
 			h->clearEditDraft();
 		} else {
-			h->setEditDraft(std_::make_unique<HistoryEditDraft>(editText, editMsgId, editCursor, editPreviewCancelled));
+			h->setEditDraft(std_::make_unique<HistoryEditDraft>(editData, editMsgId, editCursor, editPreviewCancelled));
 		}
 	}
 
@@ -3019,7 +3031,7 @@ namespace Local {
 		} else {
 			int32 setsCount = 0;
 			QByteArray hashToWrite;
-			quint32 size = sizeof(quint32) + _bytearraySize(hashToWrite);
+			quint32 size = sizeof(quint32) + Serialize::bytearraySize(hashToWrite);
 			for (auto i = sets.cbegin(); i != sets.cend(); ++i) {
 				bool notLoaded = (i->flags & MTPDstickerSet_ClientFlag::f_not_loaded);
 				if (notLoaded) {
@@ -3682,7 +3694,7 @@ namespace Local {
 			}
 			quint32 size = sizeof(quint32);
 			for (SavedPeers::const_iterator i = saved.cbegin(); i != saved.cend(); ++i) {
-				size += _peerSize(i.key()) + _dateTimeSize();
+				size += _peerSize(i.key()) + Serialize::dateTimeSize();
 			}
 
 			EncryptedDescriptor data(size);
diff --git a/Telegram/SourceFiles/localstorage.h b/Telegram/SourceFiles/localstorage.h
index 10264515a..ee5e8ba6e 100644
--- a/Telegram/SourceFiles/localstorage.h
+++ b/Telegram/SourceFiles/localstorage.h
@@ -105,11 +105,15 @@ namespace Local {
 
 	int32 oldSettingsVersion();
 
+	using TextWithTags = FlatTextarea::TextWithTags;
 	struct MessageDraft {
-		MessageDraft(MsgId msgId = 0, QString text = QString(), bool previewCancelled = false) : msgId(msgId), text(text), previewCancelled(previewCancelled) {
+		MessageDraft(MsgId msgId = 0, TextWithTags textWithTags = TextWithTags(), bool previewCancelled = false)
+			: msgId(msgId)
+			, textWithTags(textWithTags)
+			, previewCancelled(previewCancelled) {
 		}
 		MsgId msgId;
-		QString text;
+		TextWithTags textWithTags;
 		bool previewCancelled;
 	};
 	void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft);
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index e59cae222..a724cec8f 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -159,7 +159,9 @@ bool MainWidget::onShareUrl(const PeerId &peer, const QString &url, const QStrin
 		return false;
 	}
 	History *h = App::history(peer);
-	h->setMsgDraft(std_::make_unique<HistoryDraft>(url + '\n' + text, 0, MessageCursor(url.size() + 1, url.size() + 1 + text.size(), QFIXED_MAX), false));
+	TextWithTags textWithTags = { url + '\n' + text, TextWithTags::Tags() };
+	MessageCursor cursor = { url.size() + 1, url.size() + 1 + text.size(), QFIXED_MAX };
+	h->setMsgDraft(std_::make_unique<HistoryDraft>(textWithTags, 0, cursor, false));
 	h->clearEditDraft();
 	bool opened = _history->peer() && (_history->peer()->id == peer);
 	if (opened) {
@@ -177,7 +179,9 @@ bool MainWidget::onInlineSwitchChosen(const PeerId &peer, const QString &botAndQ
 		return false;
 	}
 	History *h = App::history(peer);
-	h->setMsgDraft(std_::make_unique<HistoryDraft>(botAndQuery, 0, MessageCursor(botAndQuery.size(), botAndQuery.size(), QFIXED_MAX), false));
+	TextWithTags textWithTags = { botAndQuery, TextWithTags::Tags() };
+	MessageCursor cursor = { botAndQuery.size(), botAndQuery.size(), QFIXED_MAX };
+	h->setMsgDraft(std_::make_unique<HistoryDraft>(textWithTags, 0, cursor, false));
 	h->clearEditDraft();
 	bool opened = _history->peer() && (_history->peer()->id == peer);
 	if (opened) {
@@ -1086,7 +1090,7 @@ void executeParsedCommand(const QString &command) {
 
 void MainWidget::sendMessage(const MessageToSend &message) {
 	auto history = message.history;
-	const auto &text = message.text;
+	const auto &textWithTags = message.textWithTags;
 
 	readServerHistory(history, false);
 	_history->fastShowAtEnd(history);
@@ -1095,13 +1099,13 @@ void MainWidget::sendMessage(const MessageToSend &message) {
 		return;
 	}
 
-	saveRecentHashtags(text);
+	saveRecentHashtags(textWithTags.text);
 
-	EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(message.entities);
+	EntitiesInText sendingEntities, leftEntities = entitiesFromTextTags(textWithTags.tags);
 	auto prepareFlags = itemTextOptions(history, App::self()).flags;
-	QString sendingText, leftText = prepareTextWithEntities(text, prepareFlags, &leftEntities);
+	QString sendingText, leftText = prepareTextWithEntities(textWithTags.text, prepareFlags, &leftEntities);
 
-	QString command = parseCommandFromMessage(history, text);
+	QString command = parseCommandFromMessage(history, textWithTags.text);
 	HistoryItem *lastMessage = nullptr;
 
 	MsgId replyTo = (message.replyTo < 0) ? _history->replyToId() : 0;
diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h
index 17af829aa..7da61f427 100644
--- a/Telegram/SourceFiles/mainwidget.h
+++ b/Telegram/SourceFiles/mainwidget.h
@@ -283,8 +283,7 @@ public:
 
 	struct MessageToSend {
 		History *history = nullptr;
-		QString text;
-		FlatTextarea::TagList entities;
+		TextWithTags textWithTags;
 		MsgId replyTo = 0;
 		bool broadcast = false;
 		bool silent = false;
diff --git a/Telegram/SourceFiles/pspecific_mac.cpp b/Telegram/SourceFiles/pspecific_mac.cpp
index e1e94fbd2..6ebacc89f 100644
--- a/Telegram/SourceFiles/pspecific_mac.cpp
+++ b/Telegram/SourceFiles/pspecific_mac.cpp
@@ -86,7 +86,7 @@ void MacPrivate::notifyReplied(unsigned long long peer, int msgid, const char *s
 
 	MainWidget::MessageToSend message;
 	message.history = history;
-	message.text = QString::fromUtf8(str);
+	message.textWithTags = { QString::fromUtf8(str), TextWithTags::Tags() };
 	message.replyTo = (msgid > 0 && !history->peer->isUser()) ? msgid : 0;
 	message.broadcast = false;
 	message.silent = false;
diff --git a/Telegram/SourceFiles/serialize/serialize_common.cpp b/Telegram/SourceFiles/serialize/serialize_common.cpp
index 0d7ee09f6..ea9d68e17 100644
--- a/Telegram/SourceFiles/serialize/serialize_common.cpp
+++ b/Telegram/SourceFiles/serialize/serialize_common.cpp
@@ -23,10 +23,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 
 namespace Serialize {
 
-int stringSize(const QString &str) {
-	return sizeof(quint32) + str.size() * sizeof(ushort);
-}
-
 void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc) {
 	stream << qint32(loc.width()) << qint32(loc.height());
 	stream << qint32(loc.dc()) << quint64(loc.volume()) << qint32(loc.local()) << quint64(loc.secret());
diff --git a/Telegram/SourceFiles/serialize/serialize_common.h b/Telegram/SourceFiles/serialize/serialize_common.h
index 7412104fe..dc57155f1 100644
--- a/Telegram/SourceFiles/serialize/serialize_common.h
+++ b/Telegram/SourceFiles/serialize/serialize_common.h
@@ -24,7 +24,17 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 
 namespace Serialize {
 
-int stringSize(const QString &str);
+inline int stringSize(const QString &str) {
+	return sizeof(quint32) + str.size() * sizeof(ushort);
+}
+
+inline int bytearraySize(const QByteArray &arr) {
+	return sizeof(quint32) + arr.size();
+}
+
+inline int dateTimeSize() {
+	return (sizeof(qint64) + sizeof(quint32) + sizeof(qint8));
+}
 
 void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc);
 StorageImageLocation readStorageImageLocation(QDataStream &stream);
diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp
index 3e7b4c4ca..9486b8dbf 100644
--- a/Telegram/SourceFiles/ui/flattextarea.cpp
+++ b/Telegram/SourceFiles/ui/flattextarea.cpp
@@ -23,9 +23,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 
 #include "mainwindow.h"
 
-namespace {
-
-QByteArray serializeTagsList(const FlatTextarea::TagList &tags) {
+QByteArray FlatTextarea::serializeTagsList(const TagList &tags) {
 	if (tags.isEmpty()) {
 		return QByteArray();
 	}
@@ -44,8 +42,11 @@ QByteArray serializeTagsList(const FlatTextarea::TagList &tags) {
 	return tagsSerialized;
 }
 
-FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
-	FlatTextarea::TagList result;
+FlatTextarea::TagList FlatTextarea::deserializeTagsList(QByteArray data, int textLength) {
+	TagList result;
+	if (data.isEmpty()) {
+		return result;
+	}
 
 	QBuffer buffer(&data);
 	buffer.open(QIODevice::ReadOnly);
@@ -58,7 +59,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
 	if (stream.status() != QDataStream::Ok) {
 		return result;
 	}
-	if (tagCount <= 0 || tagCount > textSize) {
+	if (tagCount <= 0 || tagCount > textLength) {
 		return result;
 	}
 
@@ -69,7 +70,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
 		if (stream.status() != QDataStream::Ok) {
 			return result;
 		}
-		if (offset < 0 || length <= 0 || offset + length > textSize) {
+		if (offset < 0 || length <= 0 || offset + length > textLength) {
 			return result;
 		}
 		result.push_back({ offset, length, id });
@@ -77,12 +78,12 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
 	return result;
 }
 
-constexpr str_const TagsMimeType = "application/x-td-field-tags";
+QString FlatTextarea::tagsMimeType() {
+	return qsl("application/x-td-field-tags");
+}
 
-} // namespace
-
-FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v) : QTextEdit(parent)
-, _oldtext(v)
+FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v, const TagList &tags) : QTextEdit(parent)
+, _lastTextWithTags { v, tags }
 , _phVisible(!v.length())
 , a_phLeft(_phVisible ? 0 : st.phShift)
 , a_phAlpha(_phVisible ? 1 : 0)
@@ -126,22 +127,41 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const
 	connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
 	if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu()));
 
-	if (!v.isEmpty()) {
-		setTextFast(v);
+	if (!_lastTextWithTags.text.isEmpty()) {
+		setTextWithTags(_lastTextWithTags, ClearUndoHistory);
 	}
 }
 
-void FlatTextarea::setTextFast(const QString &text, bool clearUndoHistory) {
-	if (clearUndoHistory) {
-		setPlainText(text);
+FlatTextarea::TextWithTags FlatTextarea::getTextWithTagsPart(int start, int end) {
+	TextWithTags result;
+	result.text = getTextPart(start, end, &result.tags);
+	return result;
+}
+
+void FlatTextarea::setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction) {
+	_insertedTags = textWithTags.tags;
+	_insertedTagsAreFromMime = false;
+	_realInsertPosition = 0;
+	_realCharsAdded = textWithTags.text.size();
+	auto doc = document();
+	auto cursor = QTextCursor(doc->docHandle(), 0);
+	if (undoHistoryAction == ClearUndoHistory) {
+		doc->setUndoRedoEnabled(false);
+		cursor.beginEditBlock();
+	} else if (undoHistoryAction == MergeWithUndoHistory) {
+		cursor.joinPreviousEditBlock();
 	} else {
-		QTextCursor c(document()->docHandle(), 0);
-		c.joinPreviousEditBlock();
-		c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
-		c.insertText(text);
-		c.movePosition(QTextCursor::End);
-		c.endEditBlock();
+		cursor.beginEditBlock();
 	}
+	cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
+	cursor.insertText(textWithTags.text);
+	cursor.movePosition(QTextCursor::End);
+	cursor.endEditBlock();
+	if (undoHistoryAction == ClearUndoHistory) {
+		doc->setUndoRedoEnabled(true);
+	}
+	_insertedTags.clear();
+	_realInsertPosition = -1;
 	finishPlaceholder();
 }
 
@@ -266,7 +286,8 @@ void FlatTextarea::paintEvent(QPaintEvent *e) {
 		p.setFont(_st.font);
 		p.setPen(a_phColor.current());
 		if (_st.phAlign == style::al_topleft && _phAfter > 0) {
-			p.drawText(_st.textMrg.left() - _fakeMargin + a_phLeft.current() + _st.font->width(getLastText().mid(0, _phAfter)), _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph);
+			int skipWidth = _st.font->width(getTextWithTags().text.mid(0, _phAfter));
+			p.drawText(_st.textMrg.left() - _fakeMargin + a_phLeft.current() + skipWidth, _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph);
 		} else {
 			QRect phRect(_st.textMrg.left() - _fakeMargin + _st.phPos.x() + a_phLeft.current(), _st.textMrg.top() - _fakeMargin + _st.phPos.y(), width() - _st.textMrg.left() - _st.textMrg.right(), height() - _st.textMrg.top() - _st.textMrg.bottom());
 			p.drawText(phRect, _ph, QTextOption(_st.phAlign));
@@ -317,7 +338,7 @@ QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInl
 	t_assert(outInlineBot != nullptr);
 	t_assert(outInlineBotUsername != nullptr);
 
-	const QString &text(getLastText());
+	auto &text = getTextWithTags().text;
 
 	int32 inlineUsernameStart = 1, inlineUsernameLength = 0, size = text.size();
 	if (size > 2 && text.at(0) == '@' && text.at(1).isLetter()) {
@@ -474,10 +495,8 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) {
 		cursor.insertText(text + ' ', format);
 	} else {
 		_insertedTags.clear();
-		if (_tagMimeProcessor) {
-			tagId = _tagMimeProcessor->mimeTagFromTag(tagId);
-		}
 		_insertedTags.push_back({ 0, text.size(), tagId });
+		_insertedTagsAreFromMime = false;
 		cursor.insertText(text + ' ');
 		_insertedTags.clear();
 	}
@@ -601,7 +620,7 @@ private:
 
 } // namespace
 
-QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const {
+QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged) const {
 	if (end >= 0 && end <= start) return QString();
 
 	if (start < 0) start = 0;
@@ -632,7 +651,7 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou
 			int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length());
 			if (!full) {
 				tillFragmentEnd = (e <= end);
-				if (p == end && outTagsList) {
+				if (p == end) {
 					tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
 				}
 				if (p >= end) {
@@ -642,7 +661,7 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou
 					continue;
 				}
 			}
-			if (outTagsList && (full || p >= start)) {
+			if (full || p >= start) {
 				tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
 			}
 
@@ -689,11 +708,9 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou
 	}
 	result.chop(1);
 
-	if (outTagsList) {
-		if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size());
-		if (outTagsChanged) {
-			*outTagsChanged = tagAccumulator.changed();
-		}
+	if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size());
+	if (outTagsChanged) {
+		*outTagsChanged = tagAccumulator.changed();
 	}
 	return result;
 }
@@ -816,11 +833,12 @@ QStringList FlatTextarea::linksList() const {
 }
 
 void FlatTextarea::insertFromMimeData(const QMimeData *source) {
-	auto mime = str_const_toString(TagsMimeType);
+	auto mime = tagsMimeType();
 	auto text = source->text();
 	if (source->hasFormat(mime)) {
 		auto tagsData = source->data(mime);
 		_insertedTags = deserializeTagsList(tagsData, text.size());
+		_insertedTagsAreFromMime = true;
 	} else {
 		_insertedTags.clear();
 	}
@@ -982,7 +1000,9 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) {
 	auto doc = document();
 
 	// Apply inserted tags.
-	int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd, _insertedTags, _tagMimeProcessor.get());
+	auto insertedTagsProcessor = _insertedTagsAreFromMime ? _tagMimeProcessor.get() : nullptr;
+	int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd,
+		_insertedTags, insertedTagsProcessor);
 	using ActionType = FormattingAction::Type;
 	while (true) {
 		FormattingAction action;
@@ -1181,15 +1201,15 @@ void FlatTextarea::onDocumentContentsChanged() {
 	if (_correcting) return;
 
 	auto tagsChanged = false;
-	auto curText = getText(0, -1, &_oldtags, &tagsChanged);
+	auto curText = getTextPart(0, -1, &_lastTextWithTags.tags, &tagsChanged);
 
 	_correcting = true;
-	correctValue(_oldtext, curText, _oldtags);
+	correctValue(_lastTextWithTags.text, curText, _lastTextWithTags.tags);
 	_correcting = false;
 
-	bool textOrTagsChanged = tagsChanged || (_oldtext != curText);
+	bool textOrTagsChanged = tagsChanged || (_lastTextWithTags.text != curText);
 	if (textOrTagsChanged) {
-		_oldtext = curText;
+		_lastTextWithTags.text = curText;
 		emit changed();
 		checkContentHeight();
 	}
@@ -1231,12 +1251,16 @@ void FlatTextarea::setPlaceholder(const QString &ph, int32 afterSymbols) {
 		_phAfter = afterSymbols;
 		updatePlaceholder();
 	}
-	_phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - (_phAfter ? _st.font->width(getLastText().mid(0, _phAfter)) : 0));
+	int skipWidth = 0;
+	if (_phAfter) {
+		skipWidth = _st.font->width(getTextWithTags().text.mid(0, _phAfter));
+	}
+	_phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - skipWidth);
 	if (_phVisible) update();
 }
 
 void FlatTextarea::updatePlaceholder() {
-	bool vis = (getLastText().size() <= _phAfter);
+	bool vis = (getTextWithTags().text.size() <= _phAfter);
 	if (vis == _phVisible) return;
 
 	a_phLeft.start(vis ? 0 : _st.phShift);
@@ -1252,14 +1276,14 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const {
 	int32 start = c.selectionStart(), end = c.selectionEnd();
 	if (end > start) {
 		TagList tags;
-		result->setText(getText(start, end, &tags, nullptr));
+		result->setText(getTextPart(start, end, &tags));
 		if (!tags.isEmpty()) {
 			if (_tagMimeProcessor) {
 				for (auto &tag : tags) {
 					tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id);
 				}
 			}
-			result->setData(str_const_toString(TagsMimeType), serializeTagsList(tags));
+			result->setData(tagsMimeType(), serializeTagsList(tags));
 		}
 	}
 	return result;
diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h
index aac48d4df..8315022ea 100644
--- a/Telegram/SourceFiles/ui/flattextarea.h
+++ b/Telegram/SourceFiles/ui/flattextarea.h
@@ -31,16 +31,27 @@ class FlatTextarea : public QTextEdit {
 
 public:
 
-	FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString());
+	struct Tag {
+		int offset, length;
+		QString id;
+	};
+	using TagList = QVector<Tag>;
+	struct TextWithTags {
+		using Tags = FlatTextarea::TagList;
+		QString text;
+		Tags tags;
+	};
+
+	static QByteArray serializeTagsList(const TagList &tags);
+	static TagList deserializeTagsList(QByteArray data, int textLength);
+	static QString tagsMimeType();
+
+	FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString(), const TagList &tags = TagList());
 
 	void setMaxLength(int32 maxLength);
 	void setMinHeight(int32 minHeight);
 	void setMaxHeight(int32 maxHeight);
 
-	const QString &getLastText() const {
-		return _oldtext;
-	}
-
 	void setPlaceholder(const QString &ph, int32 afterSymbols = 0);
 	void updatePlaceholder();
 	void finishPlaceholder();
@@ -82,18 +93,23 @@ public:
 	};
 	void setSubmitSettings(SubmitSettings settings);
 
-	void setTextFast(const QString &text, bool clearUndoHistory = true);
-
-	struct Tag {
-		int offset, length;
-		QString id;
-	};
-	using TagList = QVector<Tag>;
-	const TagList &getLastTags() const {
-		return _oldtags;
+	const TextWithTags &getTextWithTags() const {
+		return _lastTextWithTags;
 	}
+	TextWithTags getTextWithTagsPart(int start, int end = -1);
 	void insertTag(const QString &text, QString tagId = QString());
 
+	bool isEmpty() const {
+		return _lastTextWithTags.text.isEmpty();
+	}
+
+	enum UndoHistoryAction {
+		AddToUndoHistory,
+		MergeWithUndoHistory,
+		ClearUndoHistory
+	};
+	void setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction = AddToUndoHistory);
+
 	// If you need to make some preparations of tags before putting them to QMimeData
 	// (and then to clipboard or to drag-n-drop object), here is a strategy for that.
 	class TagMimeProcessor {
@@ -147,9 +163,9 @@ protected:
 
 private:
 
-	// "start" and "end" are in coordinates of text where emoji are replaced by ObjectReplacementCharacter.
-	// If "end" = -1 means get text till the end. "outTagsList" and "outTagsChanged" may be nullptr.
-	QString getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const;
+	// "start" and "end" are in coordinates of text where emoji are replaced
+	// by ObjectReplacementCharacter. If "end" = -1 means get text till the end.
+	QString getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged = nullptr) const;
 
 	void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const;
 
@@ -169,8 +185,7 @@ private:
 	int _maxLength = -1;
 	SubmitSettings _submitSettings = SubmitSettings::Enter;
 
-	QString _ph, _phelided, _oldtext;
-	TagList _oldtags;
+	QString _ph, _phelided;
 	int _phAfter = 0;
 	bool _phVisible;
 	anim::ivalue a_phLeft;
@@ -178,8 +193,11 @@ private:
 	anim::cvalue a_phColor;
 	Animation _a_appearance;
 
+	TextWithTags _lastTextWithTags;
+
 	// Tags list which we should apply while setText() call or insert from mime data.
 	TagList _insertedTags;
+	bool _insertedTagsAreFromMime;
 
 	// Override insert position and charsAdded from complex text editing
 	// (like drag-n-drop in the same text edit field).
@@ -222,6 +240,13 @@ inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) {
 	return !(a == b);
 }
 
+inline bool operator==(const FlatTextarea::TextWithTags &a, const FlatTextarea::TextWithTags &b) {
+	return (a.text == b.text) && (a.tags == b.tags);
+}
+inline bool operator!=(const FlatTextarea::TextWithTags &a, const FlatTextarea::TextWithTags &b) {
+	return !(a == b);
+}
+
 inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {
 	return (a.start == b.start) && (a.length == b.length);
 }
diff --git a/Telegram/Telegram.xcodeproj/project.pbxproj b/Telegram/Telegram.xcodeproj/project.pbxproj
index 41b3df598..9bcf3780d 100644
--- a/Telegram/Telegram.xcodeproj/project.pbxproj
+++ b/Telegram/Telegram.xcodeproj/project.pbxproj
@@ -2023,7 +2023,7 @@
 				SDKROOT = macosx;
 				SYMROOT = ./../Mac;
 				TDESKTOP_MAJOR_VERSION = 0.9;
-				TDESKTOP_VERSION = 0.9.47;
+				TDESKTOP_VERSION = 0.9.48;
 			};
 			name = Release;
 		};
@@ -2164,7 +2164,7 @@
 				SDKROOT = macosx;
 				SYMROOT = ./../Mac;
 				TDESKTOP_MAJOR_VERSION = 0.9;
-				TDESKTOP_VERSION = 0.9.47;
+				TDESKTOP_VERSION = 0.9.48;
 			};
 			name = Debug;
 		};
diff --git a/Telegram/build/version b/Telegram/build/version
index b2d958b19..406870c55 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,6 +1,6 @@
-AppVersion         9047
+AppVersion         9048
 AppVersionStrMajor 0.9
-AppVersionStrSmall 0.9.47
-AppVersionStr      0.9.47
+AppVersionStrSmall 0.9.48
+AppVersionStr      0.9.48
 AlphaChannel       1
 BetaVersion        0