From c50ade565a6ee06f95baaa06afe332ab1579f5c1 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2019 16:34:39 +0200
Subject: [PATCH] Track sending and failed messages.

---
 Telegram/SourceFiles/api/api_sending.cpp      | 193 ++++++++++++++++++
 Telegram/SourceFiles/api/api_sending.h        |  36 ++++
 Telegram/SourceFiles/apiwrap.cpp              | 162 ++++-----------
 Telegram/SourceFiles/apiwrap.h                |  12 +-
 .../boxes/peer_list_controllers.cpp           |  39 ++--
 Telegram/SourceFiles/history/history.cpp      |  47 +++--
 Telegram/SourceFiles/history/history.h        |  17 +-
 Telegram/SourceFiles/history/history_item.cpp |  14 ++
 Telegram/SourceFiles/history/history_item.h   |   8 +-
 .../SourceFiles/history/history_widget.cpp    | 155 +++++---------
 Telegram/SourceFiles/history/history_widget.h |   1 +
 .../inline_bots/inline_bot_send_data.cpp      |   6 +-
 Telegram/SourceFiles/mainwidget.cpp           |  17 --
 Telegram/SourceFiles/mainwidget.h             |   2 -
 Telegram/SourceFiles/mtproto/type_utils.h     |   5 +-
 Telegram/SourceFiles/observer_peer.h          |   1 +
 .../SourceFiles/platform/mac/mac_touchbar.mm  |   7 +-
 Telegram/SourceFiles/ui/special_buttons.cpp   |  19 +-
 Telegram/gyp/telegram_sources.txt             |   2 +
 19 files changed, 427 insertions(+), 316 deletions(-)
 create mode 100644 Telegram/SourceFiles/api/api_sending.cpp
 create mode 100644 Telegram/SourceFiles/api/api_sending.h

diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
new file mode 100644
index 000000000..2253e5f52
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -0,0 +1,193 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "api/api_sending.h"
+
+#include "base/unixtime.h"
+#include "data/data_document.h"
+#include "data/data_photo.h"
+#include "data/data_channel.h" // ChannelData::addsSignature.
+#include "data/data_user.h" // App::peerName(UserData*).
+#include "data/data_session.h"
+#include "data/data_file_origin.h"
+#include "history/history.h"
+#include "history/history_message.h" // NewMessageFlags.
+#include "ui/text/text_entity.h" // TextWithEntities.
+#include "auth_session.h"
+#include "mainwidget.h"
+#include "apiwrap.h"
+
+namespace Api {
+namespace {
+
+template <typename MediaData>
+void SendExistingMedia(
+		not_null<History*> history,
+		not_null<MediaData*> media,
+		const MTPInputMedia &inputMedia,
+		Data::FileOrigin origin,
+		TextWithEntities caption,
+		MsgId replyToId) {
+	const auto peer = history->peer;
+	const auto session = &history->session();
+	const auto api = &session->api();
+
+	auto options = ApiWrap::SendOptions(history);
+	options.clearDraft = false;
+	options.replyTo = replyToId;
+	options.generateLocal = true;
+
+	api->sendAction(options);
+
+	const auto newId = FullMsgId(peerToChannel(peer->id), clientMsgId());
+	const auto randomId = rand_value<uint64>();
+
+	auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
+	auto sendFlags = MTPmessages_SendMedia::Flags(0);
+	if (options.replyTo) {
+		flags |= MTPDmessage::Flag::f_reply_to_msg_id;
+		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
+	}
+	bool channelPost = peer->isChannel() && !peer->isMegagroup();
+	bool silentPost = channelPost && session->data().notifySilentPosts(peer);
+	if (channelPost) {
+		flags |= MTPDmessage::Flag::f_views;
+		flags |= MTPDmessage::Flag::f_post;
+	}
+	if (!channelPost) {
+		flags |= MTPDmessage::Flag::f_from_id;
+	} else if (peer->asChannel()->addsSignature()) {
+		flags |= MTPDmessage::Flag::f_post_author;
+	}
+	if (silentPost) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
+	}
+	auto messageFromId = channelPost ? 0 : session->userId();
+	auto messagePostAuthor = channelPost
+		? App::peerName(session->user())
+		: QString();
+
+	TextUtilities::Trim(caption);
+	auto sentEntities = TextUtilities::EntitiesToMTP(
+		caption.entities,
+		TextUtilities::ConvertOption::SkipLocal);
+	if (!sentEntities.v.isEmpty()) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
+	}
+	const auto replyTo = options.replyTo;
+	const auto captionText = caption.text;
+
+	session->data().registerMessageRandomId(randomId, newId);
+
+	history->addNewLocalMessage(
+		newId.msg,
+		flags,
+		0,
+		replyTo,
+		base::unixtime::now(),
+		messageFromId,
+		messagePostAuthor,
+		media,
+		caption,
+		MTPReplyMarkup());
+
+	auto failHandler = std::make_shared<Fn<void(const RPCError&, QByteArray)>>();
+	auto performRequest = [=] {
+		const auto usedFileReference = media->fileReference();
+		history->sendRequestId = api->request(MTPmessages_SendMedia(
+			MTP_flags(sendFlags),
+			peer->input,
+			MTP_int(replyTo),
+			inputMedia,
+			MTP_string(captionText),
+			MTP_long(randomId),
+			MTPReplyMarkup(),
+			sentEntities
+		)).done([=](const MTPUpdates &result) {
+			api->applyUpdates(result, randomId);
+		}).fail([=](const RPCError &error) {
+			(*failHandler)(error, usedFileReference);
+		}).afterRequest(history->sendRequestId
+		).send();
+	};
+	*failHandler = [=](const RPCError &error, QByteArray usedFileReference) {
+		if (error.code() == 400
+			&& error.type().startsWith(qstr("FILE_REFERENCE_"))) {
+			api->refreshFileReference(origin, [=](const auto &result) {
+				if (media->fileReference() != usedFileReference) {
+					performRequest();
+				} else {
+					api->sendMessageFail(error, peer, newId);
+				}
+			});
+		} else {
+			api->sendMessageFail(error, peer, newId);
+		}
+	};
+	performRequest();
+
+	if (const auto main = App::main()) {
+		main->finishForwarding(history);
+	}
+}
+
+} // namespace
+
+void SendExistingDocument(
+		not_null<History*> history,
+		not_null<DocumentData*> document) {
+	SendExistingDocument(history, document, {});
+}
+
+void SendExistingDocument(
+		not_null<History*> history,
+		not_null<DocumentData*> document,
+		TextWithEntities caption,
+		MsgId replyToId) {
+	SendExistingMedia(
+		history,
+		document,
+		MTP_inputMediaDocument(
+			MTP_flags(0),
+			document->mtpInput(),
+			MTPint()),
+		document->stickerOrGifOrigin(),
+		caption,
+		replyToId);
+
+	if (document->sticker()) {
+		if (const auto main = App::main()) {
+			main->incrementSticker(document);
+			document->session().data().notifyRecentStickersUpdated();
+		}
+	}
+}
+
+void SendExistingPhoto(
+		not_null<History*> history,
+		not_null<PhotoData*> photo) {
+	SendExistingPhoto(history, photo, {});
+}
+
+void SendExistingPhoto(
+		not_null<History*> history,
+		not_null<PhotoData*> photo,
+		TextWithEntities caption,
+		MsgId replyToId) {
+	SendExistingMedia(
+		history,
+		photo,
+		MTP_inputMediaPhoto(
+			MTP_flags(0),
+			photo->mtpInput(),
+			MTPint()),
+		Data::FileOrigin(),
+		caption,
+		replyToId);
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_sending.h b/Telegram/SourceFiles/api/api_sending.h
new file mode 100644
index 000000000..7b8ac8d94
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_sending.h
@@ -0,0 +1,36 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+class History;
+class DocumentData;
+struct TextWithEntities;
+
+namespace Api {
+
+void SendExistingDocument(
+	not_null<History*> history,
+	not_null<DocumentData*> document);
+
+void SendExistingDocument(
+	not_null<History*> history,
+	not_null<DocumentData*> document,
+	TextWithEntities caption,
+	MsgId replyToId = 0);
+
+void SendExistingPhoto(
+	not_null<History*> history,
+	not_null<PhotoData*> photo);
+
+void SendExistingPhoto(
+	not_null<History*> history,
+	not_null<PhotoData*> photo,
+	TextWithEntities caption,
+	MsgId replyToId = 0);
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 582422b19..bf61796bd 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -549,8 +549,9 @@ void ApiWrap::toggleHistoryArchived(
 //}
 
 void ApiWrap::sendMessageFail(
+		const RPCError &error,
 		not_null<PeerData*> peer,
-		const RPCError &error) {
+		FullMsgId itemId) {
 	if (error.type() == qstr("PEER_FLOOD")) {
 		Ui::show(Box<InformBox>(
 			PeerFloodErrorText(PeerFloodType::Send)));
@@ -571,6 +572,9 @@ void ApiWrap::sendMessageFail(
 				base::unixtime::now() - (left - seconds));
 		}
 	}
+	if (const auto item = _session->data().message(itemId)) {
+		item->sendFailed();
+	}
 }
 
 void ApiWrap::requestMessageData(ChannelData *channel, MsgId msgId, RequestMessageDataCallback callback) {
@@ -4452,6 +4456,7 @@ void ApiWrap::forwardMessages(
 	auto currentGroupId = items.front()->groupId();
 	auto ids = QVector<MTPint>();
 	auto randomIds = QVector<MTPlong>();
+	auto localIds = std::unique_ptr<std::vector<FullMsgId>>();
 
 	const auto sendAccumulated = [&] {
 		if (shared) {
@@ -4473,12 +4478,21 @@ void ApiWrap::forwardMessages(
 			if (shared && !--shared->requestsLeft) {
 				shared->callback();
 			}
+		}).fail([=, ids = std::move(localIds)](const RPCError &error) {
+			if (ids) {
+				for (const auto &itemId : *ids) {
+					sendMessageFail(error, peer, itemId);
+				}
+			} else {
+				sendMessageFail(error, peer);
+			}
 		}).afterRequest(
 			history->sendRequestId
 		).send();
 
 		ids.resize(0);
 		randomIds.resize(0);
+		localIds = nullptr;
 	};
 
 	ids.reserve(count);
@@ -4486,7 +4500,7 @@ void ApiWrap::forwardMessages(
 	for (const auto item : items) {
 		auto randomId = rand_value<uint64>();
 		if (genClientSideMessage) {
-			if (auto message = item->toHistoryMessage()) {
+			if (const auto message = item->toHistoryMessage()) {
 				const auto newId = FullMsgId(
 					peerToChannel(peer->id),
 					clientMsgId());
@@ -4497,7 +4511,7 @@ void ApiWrap::forwardMessages(
 				const auto messagePostAuthor = channelPost
 					? App::peerName(self)
 					: QString();
-				history->addNewForwarded(
+				history->addNewLocalMessage(
 					newId.msg,
 					flags,
 					base::unixtime::now(),
@@ -4505,6 +4519,10 @@ void ApiWrap::forwardMessages(
 					messagePostAuthor,
 					message);
 				_session->data().registerMessageRandomId(randomId, newId);
+				if (!localIds) {
+					localIds = std::make_unique<std::vector<FullMsgId>>();
+				}
+				localIds->push_back(newId);
 			}
 		}
 		const auto newFrom = item->history()->peer;
@@ -4864,7 +4882,7 @@ void ApiWrap::editUploadedFile(
 				Box<InformBox>(tr::lng_edit_media_invalid_file(tr::now)),
 				LayerOption::KeepOther);
 		} else {
-			sendMessageFail(peer, error);
+			sendMessageFail(error, peer);
 		}
 	}).send();
 }
@@ -4997,7 +5015,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 			if (error.type() == qstr("MESSAGE_EMPTY")) {
 				lastMessage->destroy();
 			} else {
-				sendMessageFail(peer, error);
+				sendMessageFail(error, peer, newId);
 			}
 			history->clearSentDraftText(QString());
 		}).afterRequest(history->sendRequestId
@@ -5110,7 +5128,7 @@ void ApiWrap::sendInlineResult(
 		applyUpdates(result, randomId);
 		history->clearSentDraftText(QString());
 	}).fail([=](const RPCError &error) {
-		sendMessageFail(peer, error);
+		sendMessageFail(error, peer, newId);
 		history->clearSentDraftText(QString());
 	}).afterRequest(history->sendRequestId
 	).send();
@@ -5120,116 +5138,6 @@ void ApiWrap::sendInlineResult(
 	}
 }
 
-void ApiWrap::sendExistingDocument(
-		not_null<DocumentData*> document,
-		Data::FileOrigin origin,
-		TextWithEntities caption,
-		const SendOptions &options) {
-	sendAction(options);
-
-	const auto history = options.history;
-	const auto peer = history->peer;
-	const auto newId = FullMsgId(peerToChannel(peer->id), clientMsgId());
-	const auto randomId = rand_value<uint64>();
-
-	auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
-	auto sendFlags = MTPmessages_SendMedia::Flags(0);
-	if (options.replyTo) {
-		flags |= MTPDmessage::Flag::f_reply_to_msg_id;
-		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
-	}
-	bool channelPost = peer->isChannel() && !peer->isMegagroup();
-	bool silentPost = channelPost
-		&& _session->data().notifySilentPosts(peer);
-	if (channelPost) {
-		flags |= MTPDmessage::Flag::f_views;
-		flags |= MTPDmessage::Flag::f_post;
-	}
-	if (!channelPost) {
-		flags |= MTPDmessage::Flag::f_from_id;
-	} else if (peer->asChannel()->addsSignature()) {
-		flags |= MTPDmessage::Flag::f_post_author;
-	}
-	if (silentPost) {
-		sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
-	}
-	auto messageFromId = channelPost ? 0 : _session->userId();
-	auto messagePostAuthor = channelPost
-		? App::peerName(_session->user())
-		: QString();
-
-	TextUtilities::Trim(caption);
-	auto sentEntities = TextUtilities::EntitiesToMTP(
-		caption.entities,
-		TextUtilities::ConvertOption::SkipLocal);
-	if (!sentEntities.v.isEmpty()) {
-		sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
-	}
-	const auto replyTo = options.replyTo;
-	const auto captionText = caption.text;
-
-	_session->data().registerMessageRandomId(randomId, newId);
-
-	history->addNewDocument(
-		newId.msg,
-		flags,
-		0,
-		replyTo,
-		base::unixtime::now(),
-		messageFromId,
-		messagePostAuthor,
-		document,
-		caption,
-		MTPReplyMarkup());
-
-	auto failHandler = std::make_shared<Fn<void(const RPCError&, QByteArray)>>();
-	auto performRequest = [=] {
-		const auto usedFileReference = document->fileReference();
-		history->sendRequestId = request(MTPmessages_SendMedia(
-			MTP_flags(sendFlags),
-			peer->input,
-			MTP_int(replyTo),
-			MTP_inputMediaDocument(
-				MTP_flags(0),
-				document->mtpInput(),
-				MTPint()),
-			MTP_string(captionText),
-			MTP_long(randomId),
-			MTPReplyMarkup(),
-			sentEntities
-		)).done([=](const MTPUpdates &result) {
-			applyUpdates(result, randomId);
-		}).fail([=](const RPCError &error) {
-			(*failHandler)(error, usedFileReference);
-		}).afterRequest(history->sendRequestId
-		).send();
-	};
-	*failHandler = [=](const RPCError &error, QByteArray usedFileReference) {
-		if (error.code() == 400
-			&& error.type().startsWith(qstr("FILE_REFERENCE_"))) {
-			auto refreshed = [=](const UpdatedFileReferences &data) {
-				if (document->fileReference() != usedFileReference) {
-					performRequest();
-				} else {
-					sendMessageFail(peer, error);
-				}
-			};
-			refreshFileReference(origin, std::move(refreshed));
-		} else {
-			sendMessageFail(peer, error);
-		}
-	};
-	performRequest();
-
-	if (const auto main = App::main()) {
-		main->finishForwarding(history);
-		if (document->sticker()) {
-			main->incrementSticker(document);
-			_session->data().notifyRecentStickersUpdated();
-		}
-	}
-}
-
 void ApiWrap::uploadAlbumMedia(
 		not_null<HistoryItem*> item,
 		const MessageGroupId &groupId,
@@ -5341,6 +5249,7 @@ void ApiWrap::sendMediaWithRandomId(
 			: MTPmessages_SendMedia::Flag(0));
 
 	const auto peer = history->peer;
+	const auto itemId = item->fullId();
 	history->sendRequestId = request(MTPmessages_SendMedia(
 		MTP_flags(flags),
 		peer->input,
@@ -5350,9 +5259,12 @@ void ApiWrap::sendMediaWithRandomId(
 		MTP_long(randomId),
 		MTPReplyMarkup(),
 		sentEntities
-	)).done([=](const MTPUpdates &result) { applyUpdates(result);
-	}).fail([=](const RPCError &error) { sendMessageFail(peer, error);
-	}).afterRequest(history->sendRequestId
+	)).done([=](const MTPUpdates &result) {
+		applyUpdates(result);
+	}).fail([=](const RPCError &error) {
+		sendMessageFail(error, peer, itemId);
+	}).afterRequest(
+		history->sendRequestId
 	).send();
 }
 
@@ -5438,9 +5350,15 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
 		_sendingAlbums.remove(groupId);
 		applyUpdates(result);
 	}).fail([=](const RPCError &error) {
-		_sendingAlbums.remove(groupId);
-		sendMessageFail(peer, error);
-	}).afterRequest(history->sendRequestId
+		if (const auto album = _sendingAlbums.take(groupId)) {
+			for (const auto &item : (*album)->items) {
+				sendMessageFail(error, peer, item.msgId);
+			}
+		} else {
+			sendMessageFail(error, peer);
+		}
+	}).afterRequest(
+		history->sendRequestId
 	).send();
 }
 
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index b72ec4232..374917042 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -459,11 +459,10 @@ public:
 		not_null<UserData*> bot,
 		not_null<InlineBots::Result*> data,
 		const SendOptions &options);
-	void sendExistingDocument(
-		not_null<DocumentData*> document,
-		Data::FileOrigin origin,
-		TextWithEntities caption,
-		const SendOptions &options);
+	void sendMessageFail(
+		const RPCError &error,
+		not_null<PeerData*> peer,
+		FullMsgId itemId = FullMsgId());
 
 	void requestSupportContact(FnMut<void(const MTPUser&)> callback);
 
@@ -662,9 +661,6 @@ private:
 		not_null<ChannelData*> channel,
 		not_null<UserData*> from);
 
-	void sendMessageFail(
-		not_null<PeerData*> peer,
-		const RPCError &error);
 	void uploadAlbumMedia(
 		not_null<HistoryItem*> item,
 		const MessageGroupId &groupId,
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index e9652cb72..6aee81a4d 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -29,24 +29,27 @@ namespace {
 void ShareBotGame(not_null<UserData*> bot, not_null<PeerData*> chat) {
 	const auto history = chat->owner().historyLoaded(chat);
 	const auto randomId = rand_value<uint64>();
-	const auto requestId = MTP::send(
-		MTPmessages_SendMedia(
-			MTP_flags(0),
-			chat->input,
-			MTP_int(0),
-			MTP_inputMediaGame(
-				MTP_inputGameShortName(
-					bot->inputUser,
-					MTP_string(bot->botInfo->shareGameShortName))),
-			MTP_string(),
-			MTP_long(randomId),
-			MTPReplyMarkup(),
-			MTPVector<MTPMessageEntity>()),
-		App::main()->rpcDone(&MainWidget::sentUpdatesReceived),
-		App::main()->rpcFail(&MainWidget::sendMessageFail),
-		0,
-		0,
-		history ? history->sendRequestId : 0);
+	const auto api = &chat->session().api();
+	const auto requestId = api->request(MTPmessages_SendMedia(
+		MTP_flags(0),
+		chat->input,
+		MTP_int(0),
+		MTP_inputMediaGame(
+			MTP_inputGameShortName(
+				bot->inputUser,
+				MTP_string(bot->botInfo->shareGameShortName))),
+		MTP_string(),
+		MTP_long(randomId),
+		MTPReplyMarkup(),
+		MTPVector<MTPMessageEntity>()
+	)).done([=](const MTPUpdates &result) {
+		api->applyUpdates(result, randomId);
+	}).fail([=](const RPCError &error) {
+		api->sendMessageFail(error, chat);
+	}).afterRequest(
+		history ? history->sendRequestId : 0
+	).send();
+
 	if (history) {
 		history->sendRequestId = requestId;
 	}
diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index a1a46b96a..0c9b02f0b 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -614,18 +614,6 @@ std::vector<not_null<HistoryItem*>> History::createItems(
 	return result;
 }
 
-not_null<HistoryItem*> History::addNewService(
-		MsgId msgId,
-		TimeId date,
-		const QString &text,
-		MTPDmessage::Flags flags,
-		bool unread) {
-	auto message = HistoryService::PreparedText { text };
-	return addNewItem(
-		new HistoryService(this, msgId, date, message, flags),
-		unread);
-}
-
 HistoryItem *History::addNewMessage(
 		const MTPMessage &msg,
 		NewMessageType type) {
@@ -696,13 +684,13 @@ HistoryItem *History::addToHistory(const MTPMessage &msg) {
 	return createItem(msg, detachExistingItem);
 }
 
-not_null<HistoryItem*> History::addNewForwarded(
+not_null<HistoryItem*> History::addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		TimeId date,
 		UserId from,
 		const QString &postAuthor,
-		not_null<HistoryMessage*> original) {
+		not_null<HistoryMessage*> forwardOriginal) {
 	return addNewItem(
 		owner().makeMessage(
 			this,
@@ -711,11 +699,11 @@ not_null<HistoryItem*> History::addNewForwarded(
 			date,
 			from,
 			postAuthor,
-			original),
+			forwardOriginal),
 		true);
 }
 
-not_null<HistoryItem*> History::addNewDocument(
+not_null<HistoryItem*> History::addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		UserId viaBotId,
@@ -742,7 +730,7 @@ not_null<HistoryItem*> History::addNewDocument(
 		true);
 }
 
-not_null<HistoryItem*> History::addNewPhoto(
+not_null<HistoryItem*> History::addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		UserId viaBotId,
@@ -769,7 +757,7 @@ not_null<HistoryItem*> History::addNewPhoto(
 		true);
 }
 
-not_null<HistoryItem*> History::addNewGame(
+not_null<HistoryItem*> History::addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		UserId viaBotId,
@@ -1260,10 +1248,33 @@ void History::newItemAdded(not_null<HistoryItem*> item) {
 
 void History::registerLocalMessage(not_null<HistoryItem*> item) {
 	_localMessages.emplace(item);
+	if (peer->isChannel()) {
+		Notify::peerUpdatedDelayed(
+			peer,
+			Notify::PeerUpdate::Flag::ChannelLocalMessages);
+	}
 }
 
 void History::unregisterLocalMessage(not_null<HistoryItem*> item) {
 	_localMessages.remove(item);
+	if (peer->isChannel()) {
+		Notify::peerUpdatedDelayed(
+			peer,
+			Notify::PeerUpdate::Flag::ChannelLocalMessages);
+	}
+}
+
+HistoryItem *History::latestSendingMessage() const {
+	auto sending = ranges::view::all(
+		_localMessages
+	) | ranges::view::filter([](not_null<HistoryItem*> item) {
+		return item->isSending();
+	});
+	const auto i = ranges::max_element(sending, ranges::less(), [](
+			not_null<HistoryItem*> item) {
+		return uint64(item->date()) << 32 | uint32(item->id);
+	});
+	return (i == sending.end()) ? nullptr : i->get();
 }
 
 HistoryBlock *History::prepareBlockForAddingItem() {
diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h
index 14c852cc4..c52a6621d 100644
--- a/Telegram/SourceFiles/history/history.h
+++ b/Telegram/SourceFiles/history/history.h
@@ -95,20 +95,14 @@ public:
 
 	HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type);
 	HistoryItem *addToHistory(const MTPMessage &msg);
-	not_null<HistoryItem*> addNewService(
-		MsgId msgId,
-		TimeId date,
-		const QString &text,
-		MTPDmessage::Flags flags = 0,
-		bool newMsg = true);
-	not_null<HistoryItem*> addNewForwarded(
+	not_null<HistoryItem*> addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		TimeId date,
 		UserId from,
 		const QString &postAuthor,
-		not_null<HistoryMessage*> original);
-	not_null<HistoryItem*> addNewDocument(
+		not_null<HistoryMessage*> forwardOriginal);
+	not_null<HistoryItem*> addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		UserId viaBotId,
@@ -119,7 +113,7 @@ public:
 		not_null<DocumentData*> document,
 		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup);
-	not_null<HistoryItem*> addNewPhoto(
+	not_null<HistoryItem*> addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		UserId viaBotId,
@@ -130,7 +124,7 @@ public:
 		not_null<PhotoData*> photo,
 		const TextWithEntities &caption,
 		const MTPReplyMarkup &markup);
-	not_null<HistoryItem*> addNewGame(
+	not_null<HistoryItem*> addNewLocalMessage(
 		MsgId id,
 		MTPDmessage::Flags flags,
 		UserId viaBotId,
@@ -155,6 +149,7 @@ public:
 
 	void registerLocalMessage(not_null<HistoryItem*> item);
 	void unregisterLocalMessage(not_null<HistoryItem*> item);
+	[[nodiscard]] HistoryItem *latestSendingMessage() const;
 
 	MsgId readInbox();
 	void applyInboxReadUpdate(
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index fa7790a26..e8109eb89 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_channel.h"
 #include "data/data_chat.h"
 #include "data/data_user.h"
+#include "observer_peer.h"
 #include "styles/style_dialogs.h"
 #include "styles/style_history.h"
 
@@ -664,6 +665,19 @@ MsgId HistoryItem::idOriginal() const {
 	return id;
 }
 
+void HistoryItem::sendFailed() {
+	Expects(_flags & MTPDmessage_ClientFlag::f_sending);
+	Expects(!(_flags & MTPDmessage_ClientFlag::f_failed));
+
+	_flags = (_flags | MTPDmessage_ClientFlag::f_failed)
+		& ~MTPDmessage_ClientFlag::f_sending;
+	if (history()->peer->isChannel()) {
+		Notify::peerUpdatedDelayed(
+			history()->peer,
+			Notify::PeerUpdate::Flag::ChannelLocalMessages);
+	}
+}
+
 bool HistoryItem::needCheck() const {
 	return out() || (id < 0 && history()->peer->isSelf());
 }
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index 9d8f718e8..818d25331 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -122,7 +122,6 @@ public:
 	[[nodiscard]] bool hasUnreadMediaFlag() const;
 	void markMediaRead();
 
-
 	// For edit media in history_message.
 	virtual void returnSavedMedia() {};
 	void savePreviousMedia() {
@@ -174,6 +173,13 @@ public:
 	bool isSilent() const {
 		return _flags & MTPDmessage::Flag::f_silent;
 	}
+	bool isSending() const {
+		return _flags & MTPDmessage_ClientFlag::f_sending;
+	}
+	bool hasFailed() const {
+		return _flags & MTPDmessage_ClientFlag::f_failed;
+	}
+	void sendFailed();
 	virtual int viewsCount() const {
 		return hasViews() ? 1 : -1;
 	}
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 59fb7266c..1fa4664b9 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "history/history_widget.h"
 
+#include "api/api_sending.h"
 #include "boxes/confirm_box.h"
 #include "boxes/send_files_box.h"
 #include "boxes/share_box.h"
@@ -236,7 +237,7 @@ object_ptr<Ui::FlatButton> SetupDiscussButton(
 	return result;
 }
 
-void ShowSlowmodeToast(const QString &text) {
+void ShowErrorToast(const QString &text) {
 	auto config = Ui::Toast::Config();
 	config.multiline = true;
 	config.minWidth = st::msgMinWidth;
@@ -244,13 +245,6 @@ void ShowSlowmodeToast(const QString &text) {
 	Ui::Toast::Show(config);
 }
 
-void ShowSlowmodeToast(int seconds) {
-	ShowSlowmodeToast(tr::lng_slowmode_enabled(
-		tr::now,
-		lt_left,
-		formatDurationWords(seconds)));
-}
-
 } // namespace
 
 HistoryWidget::HistoryWidget(
@@ -511,7 +505,9 @@ HistoryWidget::HistoryWidget(
 		| UpdateFlag::NotificationsEnabled
 		| UpdateFlag::ChannelAmIn
 		| UpdateFlag::ChannelPromotedChanged
-		| UpdateFlag::ChannelLinkedChat;
+		| UpdateFlag::ChannelLinkedChat
+		| UpdateFlag::ChannelSlowmode
+		| UpdateFlag::ChannelLocalMessages;
 	subscribe(Notify::PeerUpdated(), Notify::PeerUpdatedHandler(changes, [=](const Notify::PeerUpdate &update) {
 		if (update.peer == _peer) {
 			if (update.flags & UpdateFlag::RightsChanged) {
@@ -552,6 +548,10 @@ HistoryWidget::HistoryWidget(
 				updateControlsGeometry();
 				this->update();
 			}
+			if (update.flags & (UpdateFlag::ChannelSlowmode
+				| UpdateFlag::ChannelLocalMessages)) {
+				updateSendButtonType();
+			}
 			if (update.flags & (UpdateFlag::UserIsBlocked
 				| UpdateFlag::AdminsChanged
 				| UpdateFlag::MembersChanged
@@ -2820,8 +2820,7 @@ void HistoryWidget::send(Qt::KeyboardModifiers modifiers) {
 	} else if (_editMsgId) {
 		saveEditMsg();
 		return;
-	} else if (const auto left = _peer->slowmodeSecondsLeft()) {
-		ShowSlowmodeToast(left);
+	} else if (showSlowmodeError()) {
 		return;
 	}
 
@@ -2842,7 +2841,7 @@ void HistoryWidget::send(Qt::KeyboardModifiers modifiers) {
 			|| (!_toForward.empty()
 				&& !message.textWithTags.text.isEmpty())
 			|| (message.textWithTags.text.size() > MaxMessageSize)) {
-			ShowSlowmodeToast(tr::lng_slowmode_no_many(tr::now));
+			ShowErrorToast(tr::lng_slowmode_no_many(tr::now));
 			return;
 		}
 	}
@@ -3047,8 +3046,7 @@ void HistoryWidget::chooseAttach() {
 			ChatRestriction::f_send_media)) {
 		Ui::Toast::Show(*error);
 		return;
-	} else if (const auto left = _peer->slowmodeSecondsLeft()) {
-		ShowSlowmodeToast(left);
+	} else if (showSlowmodeError()) {
 		return;
 	}
 
@@ -3167,6 +3165,8 @@ void HistoryWidget::recordStartCallback() {
 	if (error) {
 		Ui::show(Box<InformBox>(*error));
 		return;
+	} else if (showSlowmodeError()) {
+		return;
 	} else if (!Media::Capture::instance()->available()) {
 		return;
 	}
@@ -3581,6 +3581,10 @@ void HistoryWidget::updateSendButtonType() {
 			: 0;
 	}();
 	_send->setSlowmodeDelay(delay);
+	_send->setDisabled(_peer
+		&& _peer->slowmodeApplied()
+		&& (_history->latestSendingMessage() != nullptr)
+		&& (type == Type::Send || type == Type::Record));
 
 	if (delay != 0) {
 		App::CallDelayed(
@@ -4020,7 +4024,7 @@ bool HistoryWidget::showSendingFilesError(
 		return false;
 	}
 
-	ShowSlowmodeToast(text);
+	ShowErrorToast(text);
 	return true;
 }
 
@@ -4218,7 +4222,7 @@ void HistoryWidget::uploadFilesAfterConfirmation(
 			|| (!list.files.empty()
 				&& !caption.text.isEmpty()
 				&& !list.canAddCaption(isAlbum, compressImages)))) {
-		ShowSlowmodeToast(tr::lng_slowmode_no_many(tr::now));
+		ShowErrorToast(tr::lng_slowmode_no_many(tr::now));
 		return;
 	}
 
@@ -5331,6 +5335,31 @@ void HistoryWidget::replyToNextMessage() {
 	}
 }
 
+bool HistoryWidget::showSlowmodeError() {
+	const auto text = [&] {
+		if (const auto left = _peer->slowmodeSecondsLeft()) {
+			return tr::lng_slowmode_enabled(
+				tr::now,
+				lt_left,
+				formatDurationWords(left));
+		} else if (_peer->slowmodeApplied()) {
+			if (const auto item = _history->latestSendingMessage()) {
+				if (const auto view = item->mainView()) {
+					animatedScrollToItem(item->id);
+					enqueueMessageHighlight(view);
+				}
+				return tr::lng_slowmode_no_many(tr::now);
+			}
+		}
+		return QString();
+	}();
+	if (text.isEmpty()) {
+		return false;
+	}
+	ShowErrorToast(text);
+	return true;
+}
+
 void HistoryWidget::onFieldTabbed() {
 	if (_supportAutocomplete) {
 		_supportAutocomplete->activate(_field.data());
@@ -5344,8 +5373,7 @@ void HistoryWidget::sendInlineResult(
 		not_null<UserData*> bot) {
 	if (!_peer || !_peer->canWrite()) {
 		return;
-	} else if (const auto left = _peer->slowmodeSecondsLeft()) {
-		ShowSlowmodeToast(left);
+	} else if (showSlowmodeError()) {
 		return;
 	}
 
@@ -5500,18 +5528,11 @@ bool HistoryWidget::sendExistingDocument(
 		return false;
 	} else if (!_peer || !_peer->canWrite()) {
 		return false;
-	} else if (const auto left = _peer->slowmodeSecondsLeft()) {
-		ShowSlowmodeToast(left);
+	} else if (showSlowmodeError()) {
 		return false;
 	}
 
-	const auto origin = document->stickerOrGifOrigin();
-
-	auto options = ApiWrap::SendOptions(_history);
-	options.clearDraft = false;
-	options.replyTo = replyToId();
-	options.generateLocal = true;
-	session().api().sendExistingDocument(document, origin, caption, options);
+	Api::SendExistingDocument(_history, document, caption, replyToId());
 
 	if (_fieldAutocomplete->stickersShown()) {
 		clearFieldText();
@@ -5538,91 +5559,15 @@ bool HistoryWidget::sendExistingPhoto(
 		return false;
 	} else if (!_peer || !_peer->canWrite()) {
 		return false;
-	} else if (const auto left = _peer->slowmodeSecondsLeft()) {
-		ShowSlowmodeToast(left);
+	} else if (showSlowmodeError()) {
 		return false;
 	}
 
-	auto options = ApiWrap::SendOptions(_history);
-	options.clearDraft = false;
-	options.replyTo = replyToId();
-	options.generateLocal = true;
-	session().api().sendAction(options);
-
-	const auto randomId = rand_value<uint64>();
-	const auto newId = FullMsgId(_channel, clientMsgId());
-
-	auto flags = NewMessageFlags(_peer) | MTPDmessage::Flag::f_media;
-	auto sendFlags = MTPmessages_SendMedia::Flags(0);
-	if (options.replyTo) {
-		flags |= MTPDmessage::Flag::f_reply_to_msg_id;
-		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
-	}
-	bool channelPost = _peer->isChannel() && !_peer->isMegagroup();
-	bool silentPost = channelPost && session().data().notifySilentPosts(_peer);
-	if (channelPost) {
-		flags |= MTPDmessage::Flag::f_views;
-		flags |= MTPDmessage::Flag::f_post;
-	}
-	if (!channelPost) {
-		flags |= MTPDmessage::Flag::f_from_id;
-	} else if (_peer->asChannel()->addsSignature()) {
-		flags |= MTPDmessage::Flag::f_post_author;
-	}
-	if (silentPost) {
-		sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
-	}
-	auto messageFromId = channelPost ? 0 : session().userId();
-	auto messagePostAuthor = channelPost
-		? App::peerName(session().user())
-		: 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,
-		0,
-		options.replyTo,
-		base::unixtime::now(),
-		messageFromId,
-		messagePostAuthor,
-		photo,
-		caption,
-		MTPReplyMarkup());
-
-	_history->sendRequestId = MTP::send(
-		MTPmessages_SendMedia(
-			MTP_flags(sendFlags),
-			_peer->input,
-			MTP_int(options.replyTo),
-			MTP_inputMediaPhoto(
-				MTP_flags(0),
-				photo->mtpInput(),
-				MTPint()),
-			MTP_string(caption.text),
-			MTP_long(randomId),
-			MTPReplyMarkup(),
-			sentEntities),
-		App::main()->rpcDone(&MainWidget::sentUpdatesReceived),
-		App::main()->rpcFail(&MainWidget::sendMessageFail),
-		0,
-		0,
-		_history->sendRequestId);
-	App::main()->finishForwarding(_history);
-
-	_history->owner().registerMessageRandomId(randomId, newId);
+	Api::SendExistingPhoto(_history, photo, caption, replyToId());
 
 	hideSelectorControlsAnimated();
 
 	_field->setFocus();
-
 	return true;
 }
 
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index d7be48f11..13844b199 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -467,6 +467,7 @@ private:
 	void cancelReplyAfterMediaSend(bool lastKeyboardUsed);
 	void replyToPreviousMessage();
 	void replyToNextMessage();
+	[[nodiscard]] bool showSlowmodeError();
 
 	void hideSelectorControlsAnimated();
 	int countMembersDropdownHeightMax() const;
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
index d258882a1..a4f14247b 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
@@ -128,7 +128,7 @@ void SendPhoto::addToHistory(
 		MsgId replyToId,
 		const QString &postAuthor,
 		const MTPReplyMarkup &markup) const {
-	history->addNewPhoto(
+	history->addNewLocalMessage(
 		msgId,
 		flags,
 		viaBotId,
@@ -161,7 +161,7 @@ void SendFile::addToHistory(
 		MsgId replyToId,
 		const QString &postAuthor,
 		const MTPReplyMarkup &markup) const {
-	history->addNewDocument(
+	history->addNewLocalMessage(
 		msgId,
 		flags,
 		viaBotId,
@@ -208,7 +208,7 @@ void SendGame::addToHistory(
 		MsgId replyToId,
 		const QString &postAuthor,
 		const MTPReplyMarkup &markup) const {
-	history->addNewGame(
+	history->addNewLocalMessage(
 		msgId,
 		flags,
 		viaBotId,
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index b0647b7e9..82b5029c1 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -918,23 +918,6 @@ void MainWidget::removeDialog(Dialogs::Key key) {
 	_dialogs->removeDialog(key);
 }
 
-bool MainWidget::sendMessageFail(const RPCError &error) {
-	if (MTP::isDefaultHandledError(error)) return false;
-
-	if (error.type() == qstr("PEER_FLOOD")) {
-		Ui::show(Box<InformBox>(PeerFloodErrorText(PeerFloodType::Send)));
-		return true;
-	} else if (error.type() == qstr("USER_BANNED_IN_CHANNEL")) {
-		const auto link = textcmdLink(
-			Core::App().createInternalLinkFull(qsl("spambot")),
-			tr::lng_cant_more_info(tr::now));
-		const auto text = tr::lng_error_public_groups_denied(tr::now, lt_more_info, link);
-		Ui::show(Box<InformBox>(text));
-		return true;
-	}
-	return false;
-}
-
 void MainWidget::cacheBackground() {
 	if (Window::Theme::Background()->colorForFill()) {
 		return;
diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h
index 133a2ef78..43b1e0e3b 100644
--- a/Telegram/SourceFiles/mainwidget.h
+++ b/Telegram/SourceFiles/mainwidget.h
@@ -195,8 +195,6 @@ public:
 
 	void deletePhotoLayer(PhotoData *photo);
 
-	bool sendMessageFail(const RPCError &error);
-
 	// While HistoryInner is not HistoryView::ListWidget.
 	crl::time highlightStartTime(not_null<const HistoryItem*> item) const;
 
diff --git a/Telegram/SourceFiles/mtproto/type_utils.h b/Telegram/SourceFiles/mtproto/type_utils.h
index 6ddc2e86a..924728363 100644
--- a/Telegram/SourceFiles/mtproto/type_utils.h
+++ b/Telegram/SourceFiles/mtproto/type_utils.h
@@ -60,8 +60,11 @@ enum class MTPDmessage_ClientFlag : uint32 {
 	// message is an outgoing message that is being sent
 	f_sending = (1U << 23),
 
+	// message was an outgoing message and failed to be sent
+	f_failed = (1U << 22),
+
 	// update this when adding new client side flags
-	MIN_FIELD = (1U << 23),
+	MIN_FIELD = (1U << 22),
 };
 DEFINE_MTP_CLIENT_FLAGS(MTPDmessage)
 
diff --git a/Telegram/SourceFiles/observer_peer.h b/Telegram/SourceFiles/observer_peer.h
index 231342793..79f8018a6 100644
--- a/Telegram/SourceFiles/observer_peer.h
+++ b/Telegram/SourceFiles/observer_peer.h
@@ -68,6 +68,7 @@ struct PeerUpdate {
 		ChannelLinkedChat         = (1 << 20),
 		ChannelLocation           = (1 << 21),
 		ChannelSlowmode           = (1 << 22),
+		ChannelLocalMessages      = (1 << 23),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
diff --git a/Telegram/SourceFiles/platform/mac/mac_touchbar.mm b/Telegram/SourceFiles/platform/mac/mac_touchbar.mm
index 13098e4cb..55a69c923 100644
--- a/Telegram/SourceFiles/platform/mac/mac_touchbar.mm
+++ b/Telegram/SourceFiles/platform/mac/mac_touchbar.mm
@@ -11,6 +11,7 @@
 
 #include "apiwrap.h"
 #include "auth_session.h"
+#include "api/api_sending.h"
 #include "boxes/confirm_box.h"
 #include "chat_helpers/emoji_list_widget.h"
 #include "core/application.h"
@@ -809,11 +810,7 @@ void AppendEmojiPacks(std::vector<PickerScrubberItem> &to) {
 			if (const auto error = RestrictionToSendStickers()) {
 				Ui::show(Box<InformBox>(*error));
 			}
-			Auth().api().sendExistingDocument(
-				document,
-				document->stickerSetOrigin(),
-				{},
-				ApiWrap::SendOptions(chat.history()));
+			Api::SendExistingDocument(chat.history(), document);
 			return true;
 		} else if (const auto emoji = _stickers[index].emoji) {
 			if (const auto inputField = qobject_cast<QTextEdit*>(
diff --git a/Telegram/SourceFiles/ui/special_buttons.cpp b/Telegram/SourceFiles/ui/special_buttons.cpp
index 3b860bc53..cd6b43780 100644
--- a/Telegram/SourceFiles/ui/special_buttons.cpp
+++ b/Telegram/SourceFiles/ui/special_buttons.cpp
@@ -375,11 +375,15 @@ void SendButton::paintEvent(QPaintEvent *e) {
 
 void SendButton::paintRecord(Painter &p, bool over) {
 	auto recordActive = recordActiveRatio();
-	auto rippleColor = anim::color(st::historyAttachEmoji.ripple.color, st::historyRecordVoiceRippleBgActive, recordActive);
-	paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y(), &rippleColor);
+	if (!isDisabled()) {
+		auto rippleColor = anim::color(st::historyAttachEmoji.ripple.color, st::historyRecordVoiceRippleBgActive, recordActive);
+		paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y(), &rippleColor);
+	}
 
 	auto fastIcon = [&] {
-		if (recordActive == 1.) {
+		if (isDisabled()) {
+			return &st::historyRecordVoice;
+		} else if (recordActive == 1.) {
 			return &st::historyRecordVoiceActive;
 		} else if (over) {
 			return &st::historyRecordVoiceOver;
@@ -387,7 +391,7 @@ void SendButton::paintRecord(Painter &p, bool over) {
 		return &st::historyRecordVoice;
 	};
 	fastIcon()->paintInCenter(p, rect());
-	if (recordActive > 0. && recordActive < 1.) {
+	if (!isDisabled() && recordActive > 0. && recordActive < 1.) {
 		p.setOpacity(recordActive);
 		st::historyRecordVoiceActive.paintInCenter(p, rect());
 		p.setOpacity(1.);
@@ -414,7 +418,12 @@ void SendButton::paintSend(Painter &p, bool over) {
 	const auto &sendIcon = over
 		? st::historySendIconOver
 		: st::historySendIcon;
-	sendIcon.paint(p, st::historySendIconPosition, width());
+	if (isDisabled()) {
+		const auto color = st::historyRecordVoiceFg->c;
+		sendIcon.paint(p, st::historySendIconPosition, width(), color);
+	} else {
+		sendIcon.paint(p, st::historySendIconPosition, width());
+	}
 }
 
 void SendButton::paintSlowmode(Painter &p) {
diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt
index 35a4c5740..1849a457a 100644
--- a/Telegram/gyp/telegram_sources.txt
+++ b/Telegram/gyp/telegram_sources.txt
@@ -1,3 +1,5 @@
+<(src_loc)/api/api_sending.cpp
+<(src_loc)/api/api_sending.h
 <(src_loc)/boxes/peers/add_participants_box.cpp
 <(src_loc)/boxes/peers/add_participants_box.h
 <(src_loc)/boxes/peers/edit_contact_box.cpp