From 2f5cb33bf2b9b7bf3504ac32c2fdd68a13b206b3 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 6 Jul 2020 13:58:18 +0400
Subject: [PATCH] Fix finalizing media in non-active account.

---
 Telegram/CMakeLists.txt                       |   2 +
 .../SourceFiles/api/api_send_progress.cpp     | 105 ++++++++
 Telegram/SourceFiles/api/api_send_progress.h  |  88 ++++++
 Telegram/SourceFiles/api/api_sending.h        |   1 +
 Telegram/SourceFiles/data/data_types.h        |  29 --
 Telegram/SourceFiles/history/history.cpp      |   9 +-
 Telegram/SourceFiles/history/history.h        |  11 +-
 .../SourceFiles/history/history_widget.cpp    | 251 ++----------------
 Telegram/SourceFiles/history/history_widget.h |  36 ---
 Telegram/SourceFiles/main/main_session.cpp    |   2 +
 Telegram/SourceFiles/main/main_session.h      |   5 +
 Telegram/SourceFiles/storage/file_upload.cpp  | 141 +++++++++-
 Telegram/SourceFiles/storage/file_upload.h    |   7 +
 .../ui/effects/send_action_animations.cpp     |   5 +-
 .../ui/effects/send_action_animations.h       |   8 +-
 15 files changed, 396 insertions(+), 304 deletions(-)
 create mode 100644 Telegram/SourceFiles/api/api_send_progress.cpp
 create mode 100644 Telegram/SourceFiles/api/api_send_progress.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 93b483087..7bfd6ff7a 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -215,6 +215,8 @@ PRIVATE
     api/api_hash.h
     api/api_self_destruct.cpp
     api/api_self_destruct.h
+    api/api_send_progress.cpp
+    api/api_send_progress.h
     api/api_sending.cpp
     api/api_sending.h
     api/api_sensitive_content.cpp
diff --git a/Telegram/SourceFiles/api/api_send_progress.cpp b/Telegram/SourceFiles/api/api_send_progress.cpp
new file mode 100644
index 000000000..6666bde2e
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_send_progress.cpp
@@ -0,0 +1,105 @@
+/*
+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_send_progress.h"
+
+#include "main/main_session.h"
+#include "history/history.h"
+#include "data/data_peer.h"
+#include "apiwrap.h"
+
+namespace Api {
+namespace {
+
+constexpr auto kCancelTypingActionTimeout = crl::time(5000);
+
+} // namespace
+
+SendProgressManager::SendProgressManager(not_null<Main::Session*> session)
+: _session(session)
+, _stopTypingTimer([=] { cancelTyping(base::take(_stopTypingHistory)); }) {
+}
+
+void SendProgressManager::cancel(
+		not_null<History*> history,
+		SendProgressType type) {
+	const auto i = _requests.find({ history, type });
+	if (i != _requests.end()) {
+		_session->api().request(i->second).cancel();
+		_requests.erase(i);
+	}
+}
+
+void SendProgressManager::cancelTyping(not_null<History*> history) {
+	_stopTypingTimer.cancel();
+	cancel(history, SendProgressType::Typing);
+}
+
+void SendProgressManager::update(
+		not_null<History*> history,
+		SendProgressType type,
+		int32 progress) {
+	const auto peer = history->peer;
+	if (peer->isSelf() || (peer->isChannel() && !peer->isMegagroup())) {
+		return;
+	}
+
+	const auto doing = (progress >= 0);
+	if (history->mySendActionUpdated(type, doing)) {
+		cancel(history, type);
+		if (doing) {
+			send(history, type, progress);
+		}
+	}
+}
+
+void SendProgressManager::send(
+		not_null<History*> history,
+		SendProgressType type,
+		int32 progress) {
+	using Type = SendProgressType;
+	MTPsendMessageAction action;
+	switch (type) {
+	case Type::Typing: action = MTP_sendMessageTypingAction(); break;
+	case Type::RecordVideo: action = MTP_sendMessageRecordVideoAction(); break;
+	case Type::UploadVideo: action = MTP_sendMessageUploadVideoAction(MTP_int(progress)); break;
+	case Type::RecordVoice: action = MTP_sendMessageRecordAudioAction(); break;
+	case Type::UploadVoice: action = MTP_sendMessageUploadAudioAction(MTP_int(progress)); break;
+	case Type::RecordRound: action = MTP_sendMessageRecordRoundAction(); break;
+	case Type::UploadRound: action = MTP_sendMessageUploadRoundAction(MTP_int(progress)); break;
+	case Type::UploadPhoto: action = MTP_sendMessageUploadPhotoAction(MTP_int(progress)); break;
+	case Type::UploadFile: action = MTP_sendMessageUploadDocumentAction(MTP_int(progress)); break;
+	case Type::ChooseLocation: action = MTP_sendMessageGeoLocationAction(); break;
+	case Type::ChooseContact: action = MTP_sendMessageChooseContactAction(); break;
+	case Type::PlayGame: action = MTP_sendMessageGamePlayAction(); break;
+	}
+	const auto requestId = _session->api().request(MTPmessages_SetTyping(
+		history->peer->input,
+		action
+	)).done([=](const MTPBool &result, mtpRequestId requestId) {
+		done(result, requestId);
+	}).send();
+	_requests.emplace(Key{ history, type }, requestId);
+
+	if (type == Type::Typing) {
+		_stopTypingHistory = history;
+		_stopTypingTimer.callOnce(kCancelTypingActionTimeout);
+	}
+}
+
+void SendProgressManager::done(
+		const MTPBool &result,
+		mtpRequestId requestId) {
+	for (auto i = _requests.begin(), e = _requests.end(); i != e; ++i) {
+		if (i->second == requestId) {
+			_requests.erase(i);
+			break;
+		}
+	}
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_send_progress.h b/Telegram/SourceFiles/api/api_send_progress.h
new file mode 100644
index 000000000..14b409a43
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_send_progress.h
@@ -0,0 +1,88 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "api/api_common.h"
+#include "base/timer.h"
+
+class History;
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Api {
+
+enum class SendProgressType {
+	Typing,
+	RecordVideo,
+	UploadVideo,
+	RecordVoice,
+	UploadVoice,
+	RecordRound,
+	UploadRound,
+	UploadPhoto,
+	UploadFile,
+	ChooseLocation,
+	ChooseContact,
+	PlayGame,
+};
+
+struct SendProgress {
+	SendProgress(
+		SendProgressType type,
+		crl::time until,
+		int progress = 0)
+	: type(type)
+	, until(until)
+	, progress(progress) {
+	}
+	SendProgressType type = SendProgressType::Typing;
+	crl::time until = 0;
+	int progress = 0;
+
+};
+
+class SendProgressManager final {
+public:
+	SendProgressManager(not_null<Main::Session*> session);
+
+	void update(
+		not_null<History*> history,
+		SendProgressType type,
+		int32 progress = 0);
+	void cancel(
+		not_null<History*> history,
+		SendProgressType type);
+	void cancelTyping(not_null<History*> history);
+
+private:
+	struct Key {
+		not_null<History*> history;
+		SendProgressType type = SendProgressType();
+
+		inline bool operator<(const Key &other) const {
+			return (history < other.history)
+				|| (history == other.history && type < other.type);
+		}
+	};
+
+	void send(
+		not_null<History*> history,
+		SendProgressType type,
+		int32 progress);
+	void done(const MTPBool &result, mtpRequestId requestId);
+
+	const not_null<Main::Session*> _session;
+	base::flat_map<Key, mtpRequestId> _requests;
+	base::Timer _stopTypingTimer;
+	History *_stopTypingHistory = nullptr;
+
+};
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_sending.h b/Telegram/SourceFiles/api/api_sending.h
index 96458281b..29184faea 100644
--- a/Telegram/SourceFiles/api/api_sending.h
+++ b/Telegram/SourceFiles/api/api_sending.h
@@ -15,6 +15,7 @@ struct FileLoadResult;
 namespace Api {
 
 struct MessageToSend;
+struct SendAction;
 
 void SendExistingDocument(
 	Api::MessageToSend &&message,
diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h
index 6ac30c572..b66ea8918 100644
--- a/Telegram/SourceFiles/data/data_types.h
+++ b/Telegram/SourceFiles/data/data_types.h
@@ -422,35 +422,6 @@ inline bool operator==(
 		&& (a.scroll == b.scroll);
 }
 
-struct SendAction {
-	enum class Type {
-		Typing,
-		RecordVideo,
-		UploadVideo,
-		RecordVoice,
-		UploadVoice,
-		RecordRound,
-		UploadRound,
-		UploadPhoto,
-		UploadFile,
-		ChooseLocation,
-		ChooseContact,
-		PlayGame,
-	};
-	SendAction(
-		Type type,
-		crl::time until,
-		int progress = 0)
-	: type(type)
-	, until(until)
-	, progress(progress) {
-	}
-	Type type = Type::Typing;
-	crl::time until = 0;
-	int progress = 0;
-
-};
-
 class FileClickHandler : public LeftButtonClickHandler {
 public:
 	FileClickHandler(
diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index 6dc719a42..ace095dba 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "history/history.h"
 
+#include "api/api_send_progress.h"
 #include "history/view/history_view_element.h"
 #include "history/history_message.h"
 #include "history/history_service.h"
@@ -357,7 +358,7 @@ bool History::updateSendActionNeedsAnimating(
 		return false;
 	}
 
-	using Type = SendAction::Type;
+	using Type = Api::SendProgressType;
 	if (action.type() == mtpc_sendMessageCancelAction) {
 		clearSendAction(user);
 		return false;
@@ -420,7 +421,7 @@ bool History::updateSendActionNeedsAnimating(
 	return updateSendActionNeedsAnimating(now, true);
 }
 
-bool History::mySendActionUpdated(SendAction::Type type, bool doing) {
+bool History::mySendActionUpdated(Api::SendProgressType type, bool doing) {
 	const auto now = crl::now();
 	const auto i = _mySendActions.find(type);
 	if (doing) {
@@ -508,7 +509,7 @@ bool History::updateSendActionNeedsAnimating(crl::time now, bool force) {
 					begin(_typing)->first->firstName);
 		} else if (!_sendActions.empty()) {
 			// Handles all actions except game playing.
-			using Type = SendAction::Type;
+			using Type = Api::SendProgressType;
 			auto sendActionString = [](Type type, const QString &name) -> QString {
 				switch (type) {
 				case Type::RecordVideo: return name.isEmpty() ? tr::lng_send_action_record_video(tr::now) : tr::lng_user_action_record_video(tr::now, lt_user, name);
@@ -562,7 +563,7 @@ bool History::updateSendActionNeedsAnimating(crl::time now, bool force) {
 			}
 		}
 		if (typingCount > 0) {
-			_sendActionAnimation.start(SendAction::Type::Typing);
+			_sendActionAnimation.start(Api::SendProgressType::Typing);
 		} else if (newTypingString.isEmpty()) {
 			_sendActionAnimation.stop();
 		}
diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h
index 0687f305b..71878c028 100644
--- a/Telegram/SourceFiles/history/history.h
+++ b/Telegram/SourceFiles/history/history.h
@@ -23,6 +23,11 @@ class HistoryItem;
 class HistoryMessage;
 class HistoryService;
 
+namespace Api {
+enum class SendProgressType;
+struct SendProgress;
+} // namespace Api
+
 namespace Main {
 class Session;
 } // namespace Main
@@ -266,7 +271,7 @@ public:
 	bool hasPendingResizedItems() const;
 	void setHasPendingResizedItems();
 
-	bool mySendActionUpdated(SendAction::Type type, bool doing);
+	bool mySendActionUpdated(Api::SendProgressType type, bool doing);
 	bool paintSendAction(
 		Painter &p,
 		int x,
@@ -573,11 +578,11 @@ private:
 	QString _topPromotedType;
 
 	base::flat_map<not_null<UserData*>, crl::time> _typing;
-	base::flat_map<not_null<UserData*>, SendAction> _sendActions;
+	base::flat_map<not_null<UserData*>, Api::SendProgress> _sendActions;
 	QString _sendActionString;
 	Ui::Text::String _sendActionText;
 	Ui::SendActionAnimation _sendActionAnimation;
-	base::flat_map<SendAction::Type, crl::time> _mySendActions;
+	base::flat_map<Api::SendProgressType, crl::time> _mySendActions;
 
 	std::deque<not_null<HistoryItem*>> _notifications;
 
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 22a9bca29..874d909ac 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "api/api_sending.h"
 #include "api/api_text_entities.h"
+#include "api/api_send_progress.h"
 #include "boxes/confirm_box.h"
 #include "boxes/send_files_box.h"
 #include "boxes/share_box.h"
@@ -123,7 +124,6 @@ constexpr auto kSkipRepaintWhileScrollMs = 100;
 constexpr auto kShowMembersDropdownTimeoutMs = 300;
 constexpr auto kDisplayEditTimeWarningMs = 300 * 1000;
 constexpr auto kFullDayInMs = 86400 * 1000;
-constexpr auto kCancelTypingActionTimeout = crl::time(5000);
 constexpr auto kSaveDraftTimeout = 1000;
 constexpr auto kSaveDraftAnywayTimeout = 5000;
 constexpr auto kSaveCloudDraftIdleTimeout = 14000;
@@ -299,7 +299,6 @@ HistoryWidget::HistoryWidget(
 , _attachDragState(DragState::None)
 , _attachDragDocument(this)
 , _attachDragPhoto(this)
-, _sendActionStopTimer([this] { cancelTypingAction(); })
 , _topShadow(this) {
 	setAcceptDrops(true);
 
@@ -737,7 +736,6 @@ HistoryWidget::HistoryWidget(
 		}
 	}, lifetime());
 
-	subscribeToUploader();
 	setupScheduledToggle();
 	orderWidgets();
 	setupShortcuts();
@@ -1247,7 +1245,9 @@ void HistoryWidget::onTextChange() {
 		if (!_inlineBot
 			&& !_editMsgId
 			&& (_textUpdateEvents & TextUpdateEvent::SendTyping)) {
-			updateSendAction(_history, SendAction::Type::Typing);
+			session().sendProgressManager().update(
+				_history,
+				Api::SendProgressType::Typing);
 		}
 	}
 
@@ -1390,77 +1390,6 @@ void HistoryWidget::writeDrafts(Data::Draft **localDraft, Data::Draft **editDraf
 	}
 }
 
-void HistoryWidget::cancelSendAction(
-		not_null<History*> history,
-		SendAction::Type type) {
-	const auto i = _sendActionRequests.find({ history, type });
-	if (i != _sendActionRequests.end()) {
-		_api.request(i->second).cancel();
-		_sendActionRequests.erase(i);
-	}
-}
-
-void HistoryWidget::cancelTypingAction() {
-	if (_history) {
-		cancelSendAction(_history, SendAction::Type::Typing);
-	}
-	_sendActionStopTimer.cancel();
-}
-
-void HistoryWidget::updateSendAction(
-		not_null<History*> history,
-		SendAction::Type type,
-		int32 progress) {
-	const auto peer = history->peer;
-	if (peer->isSelf() || (peer->isChannel() && !peer->isMegagroup())) {
-		return;
-	}
-
-	const auto doing = (progress >= 0);
-	if (history->mySendActionUpdated(type, doing)) {
-		cancelSendAction(history, type);
-		if (doing) {
-			using Type = SendAction::Type;
-			MTPsendMessageAction action;
-			switch (type) {
-			case Type::Typing: action = MTP_sendMessageTypingAction(); break;
-			case Type::RecordVideo: action = MTP_sendMessageRecordVideoAction(); break;
-			case Type::UploadVideo: action = MTP_sendMessageUploadVideoAction(MTP_int(progress)); break;
-			case Type::RecordVoice: action = MTP_sendMessageRecordAudioAction(); break;
-			case Type::UploadVoice: action = MTP_sendMessageUploadAudioAction(MTP_int(progress)); break;
-			case Type::RecordRound: action = MTP_sendMessageRecordRoundAction(); break;
-			case Type::UploadRound: action = MTP_sendMessageUploadRoundAction(MTP_int(progress)); break;
-			case Type::UploadPhoto: action = MTP_sendMessageUploadPhotoAction(MTP_int(progress)); break;
-			case Type::UploadFile: action = MTP_sendMessageUploadDocumentAction(MTP_int(progress)); break;
-			case Type::ChooseLocation: action = MTP_sendMessageGeoLocationAction(); break;
-			case Type::ChooseContact: action = MTP_sendMessageChooseContactAction(); break;
-			case Type::PlayGame: action = MTP_sendMessageGamePlayAction(); break;
-			}
-			const auto requestId = _api.request(MTPmessages_SetTyping(
-				peer->input,
-				action
-			)).done([=](const MTPBool &result, mtpRequestId requestId) {
-				sendActionDone(result, requestId);
-			}).send();
-			_sendActionRequests.emplace(std::pair(history, type), requestId);
-			if (type == Type::Typing) {
-				_sendActionStopTimer.callOnce(kCancelTypingActionTimeout);
-			}
-		}
-	}
-}
-
-void HistoryWidget::sendActionDone(
-		const MTPBool &result,
-		mtpRequestId requestId) {
-	for (auto i = _sendActionRequests.begin(), e = _sendActionRequests.end(); i != e; ++i) {
-		if (i->second == requestId) {
-			_sendActionRequests.erase(i);
-			break;
-		}
-	}
-}
-
 void HistoryWidget::activate() {
 	if (_history) {
 		if (!_historyInited) {
@@ -1515,7 +1444,9 @@ void HistoryWidget::onRecordUpdate(quint16 level, qint32 samples) {
 	Core::App().updateNonIdle();
 	updateField();
 	if (_history) {
-		updateSendAction(_history, SendAction::Type::RecordVoice);
+		session().sendProgressManager().update(
+			_history,
+			Api::SendProgressType::RecordVoice);
 	}
 }
 
@@ -1795,9 +1726,12 @@ void HistoryWidget::showHistory(
 			}
 			return;
 		}
-		updateSendAction(_history, SendAction::Type::Typing, -1);
+		session().sendProgressManager().update(
+			_history,
+			Api::SendProgressType::Typing,
+			-1);
 		session().data().histories().sendPendingReadInbox(_history);
-		cancelTypingAction();
+		session().sendProgressManager().cancelTyping(_history);
 	}
 
 	clearReplyReturns();
@@ -3588,7 +3522,10 @@ void HistoryWidget::stopRecording(bool send) {
 	_recording = false;
 	_recordingSamples = 0;
 	if (_history) {
-		updateSendAction(_history, SendAction::Type::RecordVoice, -1);
+		session().sendProgressManager().update(
+			_history,
+			Api::SendProgressType::RecordVoice,
+			-1);
 	}
 
 	updateControlsVisibility();
@@ -3689,22 +3626,13 @@ void HistoryWidget::app_sendBotCallback(
 		flags |= MTPmessages_GetBotCallbackAnswer::Flag::f_data;
 		sendData = button->data;
 	}
-	const auto weak = Ui::MakeWeak(this);
 	button->requestId = session().api().request(MTPmessages_GetBotCallbackAnswer(
 		MTP_flags(flags),
 		_peer->input,
 		MTP_int(msg->id),
 		MTP_bytes(sendData)
-	)).done([info, weak](const MTPmessages_BotCallbackAnswer &result, mtpRequestId requestId) {
+	)).done([info](const MTPmessages_BotCallbackAnswer &result, mtpRequestId requestId) {
 		BotCallbackDone(info, result, requestId);
-		result.match([&](const MTPDmessages_botCallbackAnswer &data) {
-			const auto item = info.session->data().message(info.msgId);
-			if (!data.vmessage() && data.vurl() && info.game && item) {
-				if (const auto strong = weak.data()) {
-					strong->updateSendAction(item->history(), SendAction::Type::PlayGame);
-				}
-			}
-		});
 	}).fail([info](const RPCError &error, mtpRequestId requestId) {
 		BotCallbackFail(info, error, requestId);
 	}).send();
@@ -3750,6 +3678,13 @@ void HistoryWidget::BotCallbackDone(
 				UrlClickHandler::Open(link);
 			}
 		}
+		if (const auto item = info.session->data().message(info.msgId)) {
+			if (!data.vmessage() && data.vurl() && info.game) {
+				info.session->sendProgressManager().update(
+					item->history(),
+					Api::SendProgressType::PlayGame);
+			}
+		}
 	});
 }
 
@@ -4654,144 +4589,6 @@ void HistoryWidget::uploadFile(
 	session().api().sendFile(fileContent, type, action);
 }
 
-void HistoryWidget::subscribeToUploader() {
-	using namespace Storage;
-
-	session().uploader().photoReady(
-	) | rpl::start_with_next([=](const UploadedPhoto &data) {
-		if (data.edit) {
-			session().api().editUploadedFile(
-				data.fullId,
-				data.file,
-				std::nullopt,
-				data.options,
-				false);
-		} else {
-			session().api().sendUploadedPhoto(
-				data.fullId,
-				data.file,
-				data.options);
-		}
-	}, lifetime());
-
-	session().uploader().photoProgress(
-	) | rpl::start_with_next([=](const FullMsgId &fullId) {
-		photoProgress(fullId);
-	}, lifetime());
-
-	session().uploader().photoFailed(
-	) | rpl::start_with_next([=](const FullMsgId &fullId) {
-		photoFailed(fullId);
-	}, lifetime());
-
-	session().uploader().documentReady(
-	) | rpl::start_with_next([=](const UploadedDocument &data) {
-		if (data.edit) {
-			documentEdited(data.fullId, data.options, data.file);
-		} else {
-			documentUploaded(data.fullId, data.options, data.file);
-		}
-	}, lifetime());
-
-	session().uploader().thumbDocumentReady(
-	) | rpl::start_with_next([=](const UploadedThumbDocument &data) {
-		thumbDocumentUploaded(
-			data.fullId,
-			data.options,
-			data.file,
-			data.thumb,
-			data.edit);
-	}, lifetime());
-
-	session().uploader().documentProgress(
-	) | rpl::start_with_next([=](const FullMsgId &fullId) {
-		documentProgress(fullId);
-	}, lifetime());
-
-	session().uploader().documentFailed(
-	) | rpl::start_with_next([=](const FullMsgId &fullId) {
-		documentFailed(fullId);
-	}, lifetime());
-}
-
-void HistoryWidget::documentUploaded(
-		const FullMsgId &newId,
-		Api::SendOptions options,
-		const MTPInputFile &file) {
-	session().api().sendUploadedDocument(newId, file, std::nullopt, options);
-}
-
-void HistoryWidget::documentEdited(
-		const FullMsgId &newId,
-		Api::SendOptions options,
-		const MTPInputFile &file) {
-	session().api().editUploadedFile(newId, file, std::nullopt, options, true);
-}
-
-void HistoryWidget::thumbDocumentUploaded(
-		const FullMsgId &newId,
-		Api::SendOptions options,
-		const MTPInputFile &file,
-		const MTPInputFile &thumb,
-		bool edit) {
-	if (edit) {
-		session().api().editUploadedFile(newId, file, thumb, options, true);
-	} else {
-		session().api().sendUploadedDocument(newId, file, thumb, options);
-	}
-}
-
-void HistoryWidget::photoProgress(const FullMsgId &newId) {
-	if (const auto item = session().data().message(newId)) {
-		const auto photo = item->media()
-			? item->media()->photo()
-			: nullptr;
-		updateSendAction(item->history(), SendAction::Type::UploadPhoto, 0);
-		session().data().requestItemRepaint(item);
-	}
-}
-
-void HistoryWidget::documentProgress(const FullMsgId &newId) {
-	if (const auto item = session().data().message(newId)) {
-		const auto media = item->media();
-		const auto document = media ? media->document() : nullptr;
-		const auto sendAction = (document && document->isVoiceMessage())
-			? SendAction::Type::UploadVoice
-			: SendAction::Type::UploadFile;
-		const auto progress = (document && document->uploading())
-			? document->uploadingData->offset
-			: 0;
-
-		updateSendAction(
-			item->history(),
-			sendAction,
-			progress);
-		session().data().requestItemRepaint(item);
-	}
-}
-
-void HistoryWidget::photoFailed(const FullMsgId &newId) {
-	if (const auto item = session().data().message(newId)) {
-		updateSendAction(
-			item->history(),
-			SendAction::Type::UploadPhoto,
-			-1);
-		session().data().requestItemRepaint(item);
-	}
-}
-
-void HistoryWidget::documentFailed(const FullMsgId &newId) {
-	if (const auto item = session().data().message(newId)) {
-		const auto media = item->media();
-		const auto document = media ? media->document() : nullptr;
-		const auto sendAction = (document && document->isVoiceMessage())
-			? SendAction::Type::UploadVoice
-			: SendAction::Type::UploadFile;
-		updateSendAction(item->history(), sendAction, -1);
-		session().data().requestItemRepaint(item);
-	}
-}
-
 void HistoryWidget::handleHistoryChange(not_null<const History*> history) {
 	if (_list && (_history == history || _migrated == history)) {
 		handlePendingHistoryUpdate();
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index baa297a82..e75111d00 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -423,15 +423,6 @@ private:
 	void stopMessageHighlight();
 
 	auto computeSendButtonType() const;
-	void updateSendAction(
-		not_null<History*> history,
-		SendAction::Type type,
-		int32 progress = 0);
-	void cancelSendAction(
-		not_null<History*> history,
-		SendAction::Type type);
-	void cancelTypingAction();
-	void sendActionDone(const MTPBool &result, mtpRequestId requestId);
 
 	void animationCallback();
 	void updateOverStates(QPoint pos);
@@ -474,28 +465,6 @@ private:
 		Api::SendOptions options,
 		std::shared_ptr<SendingAlbum> album = nullptr);
 
-	void subscribeToUploader();
-
-	void photoProgress(const FullMsgId &msgId);
-	void photoFailed(const FullMsgId &msgId);
-	void documentUploaded(
-		const FullMsgId &msgId,
-		Api::SendOptions options,
-		const MTPInputFile &file);
-	void thumbDocumentUploaded(
-		const FullMsgId &msgId,
-		Api::SendOptions options,
-		const MTPInputFile &file,
-		const MTPInputFile &thumb,
-		bool edit = false);
-	void documentProgress(const FullMsgId &msgId);
-	void documentFailed(const FullMsgId &msgId);
-
-	void documentEdited(
-		const FullMsgId &msgId,
-		Api::SendOptions options,
-		const MTPInputFile &file);
-
 	void itemRemoved(not_null<const HistoryItem*> item);
 
 	// Updates position of controls around the message field,
@@ -800,11 +769,6 @@ private:
 	base::Timer _highlightTimer;
 	crl::time _highlightStart = 0;
 
-	base::flat_map<
-		std::pair<not_null<History*>, SendAction::Type>,
-		mtpRequestId> _sendActionRequests;
-	base::Timer _sendActionStopTimer;
-
 	crl::time _saveDraftStart = 0;
 	bool _saveDraftText = false;
 	QTimer _saveDraftTimer, _saveCloudDraftTimer;
diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp
index 95f637d3e..f59c119a0 100644
--- a/Telegram/SourceFiles/main/main_session.cpp
+++ b/Telegram/SourceFiles/main/main_session.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "apiwrap.h"
 #include "api/api_updates.h"
+#include "api/api_send_progress.h"
 #include "core/application.h"
 #include "main/main_account.h"
 #include "main/main_domain.h"
@@ -72,6 +73,7 @@ Session::Session(
 , _settings(std::move(settings))
 , _api(std::make_unique<ApiWrap>(this))
 , _updates(std::make_unique<Api::Updates>(this))
+, _sendProgressManager(std::make_unique<Api::SendProgressManager>(this))
 , _downloader(std::make_unique<Storage::DownloadManagerMtproto>(_api.get()))
 , _uploader(std::make_unique<Storage::Uploader>(_api.get()))
 , _storage(std::make_unique<Storage::Facade>())
diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h
index 01ad24f0a..712e86397 100644
--- a/Telegram/SourceFiles/main/main_session.h
+++ b/Telegram/SourceFiles/main/main_session.h
@@ -16,6 +16,7 @@ class ApiWrap;
 
 namespace Api {
 class Updates;
+class SendProgressManager;
 } // namespace Api
 
 namespace MTP {
@@ -87,6 +88,9 @@ public:
 	[[nodiscard]] Api::Updates &updates() const {
 		return *_updates;
 	}
+	[[nodiscard]] Api::SendProgressManager &sendProgressManager() const {
+		return *_sendProgressManager;
+	}
 	[[nodiscard]] Storage::DownloadManagerMtproto &downloader() const {
 		return *_downloader;
 	}
@@ -162,6 +166,7 @@ private:
 	const std::unique_ptr<SessionSettings> _settings;
 	const std::unique_ptr<ApiWrap> _api;
 	const std::unique_ptr<Api::Updates> _updates;
+	const std::unique_ptr<Api::SendProgressManager> _sendProgressManager;
 	const std::unique_ptr<Storage::DownloadManagerMtproto> _downloader;
 	const std::unique_ptr<Storage::Uploader> _uploader;
 	const std::unique_ptr<Storage::Facade> _storage;
diff --git a/Telegram/SourceFiles/storage/file_upload.cpp b/Telegram/SourceFiles/storage/file_upload.cpp
index ccaefa2c4..ffb9b1eea 100644
--- a/Telegram/SourceFiles/storage/file_upload.cpp
+++ b/Telegram/SourceFiles/storage/file_upload.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "storage/file_upload.h"
 
+#include "api/api_send_progress.h"
 #include "storage/localimageloader.h"
 #include "storage/file_download.h"
 #include "data/data_document.h"
@@ -14,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_photo.h"
 #include "data/data_session.h"
 #include "ui/image/image_location_factory.h"
+#include "history/history_item.h"
 #include "core/mime_type.h"
 #include "main/main_session.h"
 #include "apiwrap.h"
@@ -153,6 +155,141 @@ Uploader::Uploader(not_null<ApiWrap*> api)
 	connect(&nextTimer, SIGNAL(timeout()), this, SLOT(sendNext()));
 	stopSessionsTimer.setSingleShot(true);
 	connect(&stopSessionsTimer, SIGNAL(timeout()), this, SLOT(stopSessions()));
+
+	photoReady(
+	) | rpl::start_with_next([=](const UploadedPhoto &data) {
+		if (data.edit) {
+			_api->editUploadedFile(
+				data.fullId,
+				data.file,
+				std::nullopt,
+				data.options,
+				false);
+		} else {
+			_api->sendUploadedPhoto(
+				data.fullId,
+				data.file,
+				data.options);
+		}
+	}, _lifetime);
+
+	documentReady(
+	) | rpl::start_with_next([=](const UploadedDocument &data) {
+		if (data.edit) {
+			_api->editUploadedFile(
+				data.fullId,
+				data.file,
+				std::nullopt,
+				data.options,
+				true);
+		} else {
+			_api->sendUploadedDocument(
+				data.fullId,
+				data.file,
+				std::nullopt,
+				data.options);
+		}
+	}, _lifetime);
+
+	thumbDocumentReady(
+	) | rpl::start_with_next([=](const UploadedThumbDocument &data) {
+		if (data.edit) {
+			_api->editUploadedFile(
+				data.fullId,
+				data.file,
+				data.thumb,
+				data.options,
+				true);
+		} else {
+			_api->sendUploadedDocument(
+				data.fullId,
+				data.file,
+				data.thumb,
+				data.options);
+		}
+	}, _lifetime);
+
+
+	photoProgress(
+	) | rpl::start_with_next([=](const FullMsgId &fullId) {
+		processPhotoProgress(fullId);
+	}, _lifetime);
+
+	photoFailed(
+	) | rpl::start_with_next([=](const FullMsgId &fullId) {
+		processPhotoFailed(fullId);
+	}, _lifetime);
+
+	documentProgress(
+	) | rpl::start_with_next([=](const FullMsgId &fullId) {
+		processDocumentProgress(fullId);
+	}, _lifetime);
+
+	documentFailed(
+	) | rpl::start_with_next([=](const FullMsgId &fullId) {
+		processDocumentFailed(fullId);
+	}, _lifetime);
+}
+
+void Uploader::processPhotoProgress(const FullMsgId &newId) {
+	const auto session = &_api->session();
+	if (const auto item = session->data().message(newId)) {
+		const auto photo = item->media()
+			? item->media()->photo()
+			: nullptr;
+		session->sendProgressManager().update(
+			item->history(),
+			Api::SendProgressType::UploadPhoto,
+			0);
+		session->data().requestItemRepaint(item);
+	}
+}
+
+void Uploader::processDocumentProgress(const FullMsgId &newId) {
+	const auto session = &_api->session();
+	if (const auto item = session->data().message(newId)) {
+		const auto media = item->media();
+		const auto document = media ? media->document() : nullptr;
+		const auto sendAction = (document && document->isVoiceMessage())
+			? Api::SendProgressType::UploadVoice
+			: Api::SendProgressType::UploadFile;
+		const auto progress = (document && document->uploading())
+			? document->uploadingData->offset
+			: 0;
+
+		session->sendProgressManager().update(
+			item->history(),
+			sendAction,
+			progress);
+		session->data().requestItemRepaint(item);
+	}
+}
+
+void Uploader::processPhotoFailed(const FullMsgId &newId) {
+	const auto session = &_api->session();
+	if (const auto item = session->data().message(newId)) {
+		session->sendProgressManager().update(
+			item->history(),
+			Api::SendProgressType::UploadPhoto,
+			-1);
+		session->data().requestItemRepaint(item);
+	}
+}
+
+void Uploader::processDocumentFailed(const FullMsgId &newId) {
+	const auto session = &_api->session();
+	if (const auto item = session->data().message(newId)) {
+		const auto media = item->media();
+		const auto document = media ? media->document() : nullptr;
+		const auto sendAction = (document && document->isVoiceMessage())
+			? Api::SendProgressType::UploadVoice
+			: Api::SendProgressType::UploadFile;
+		session->sendProgressManager().update(
+			item->history(),
+			sendAction,
+			-1);
+		session->data().requestItemRepaint(item);
+	}
 }
 
 Uploader::~Uploader() {
@@ -282,7 +419,9 @@ void Uploader::stopSessions() {
 }
 
 void Uploader::sendNext() {
-	if (sentSize >= kMaxUploadFileParallelSize || _pausedId.msg) return;
+	if (sentSize >= kMaxUploadFileParallelSize || _pausedId.msg) {
+		return;
+	}
 
 	bool stopping = stopSessionsTimer.isActive();
 	if (queue.empty()) {
diff --git a/Telegram/SourceFiles/storage/file_upload.h b/Telegram/SourceFiles/storage/file_upload.h
index b42ecc4eb..5e2d8c1b5 100644
--- a/Telegram/SourceFiles/storage/file_upload.h
+++ b/Telegram/SourceFiles/storage/file_upload.h
@@ -121,6 +121,11 @@ private:
 	void partLoaded(const MTPBool &result, mtpRequestId requestId);
 	void partFailed(const RPCError &error, mtpRequestId requestId);
 
+	void processPhotoProgress(const FullMsgId &msgId);
+	void processPhotoFailed(const FullMsgId &msgId);
+	void processDocumentProgress(const FullMsgId &msgId);
+	void processDocumentFailed(const FullMsgId &msgId);
+
 	void currentFailed();
 
 	not_null<ApiWrap*> _api;
@@ -147,6 +152,8 @@ private:
 	rpl::event_stream<FullMsgId> _documentFailed;
 	rpl::event_stream<FullMsgId> _secureFailed;
 
+	rpl::lifetime _lifetime;
+
 };
 
 } // namespace Storage
diff --git a/Telegram/SourceFiles/ui/effects/send_action_animations.cpp b/Telegram/SourceFiles/ui/effects/send_action_animations.cpp
index 64ed0e436..46d9d0ffe 100644
--- a/Telegram/SourceFiles/ui/effects/send_action_animations.cpp
+++ b/Telegram/SourceFiles/ui/effects/send_action_animations.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "ui/effects/send_action_animations.h"
 
+#include "api/api_send_progress.h"
 #include "ui/effects/animation_value.h"
 #include "styles/style_widgets.h"
 
@@ -17,7 +18,7 @@ constexpr int kTypingDotsCount = 3;
 constexpr int kRecordArcsCount = 4;
 constexpr int kUploadArrowsCount = 3;
 
-using ImplementationsMap = QMap<SendAction::Type, const SendActionAnimation::Impl::MetaData*>;
+using ImplementationsMap = QMap<Api::SendProgressType, const SendActionAnimation::Impl::MetaData*>;
 NeverFreedPointer<ImplementationsMap> Implementations;
 
 class TypingAnimation : public SendActionAnimation::Impl {
@@ -162,7 +163,7 @@ void CreateImplementationsMap() {
 	if (Implementations) {
 		return;
 	}
-	using Type = SendAction::Type;
+	using Type = Api::SendProgressType;
 	Implementations.createIfNull();
 	Type recordTypes[] = {
 		Type::RecordVideo,
diff --git a/Telegram/SourceFiles/ui/effects/send_action_animations.h b/Telegram/SourceFiles/ui/effects/send_action_animations.h
index f0514ea98..502f59415 100644
--- a/Telegram/SourceFiles/ui/effects/send_action_animations.h
+++ b/Telegram/SourceFiles/ui/effects/send_action_animations.h
@@ -7,11 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+namespace Api {
+enum class SendProgressType;
+} // namespace Api
+
 namespace Ui {
 
 class SendActionAnimation {
 public:
-	using Type = SendAction::Type;
+	using Type = Api::SendProgressType;
 
 	void start(Type type);
 	void stop();
@@ -31,7 +35,7 @@ public:
 
 	class Impl {
 	public:
-		using Type = SendAction::Type;
+		using Type = Api::SendProgressType;
 
 		Impl(int period) : _period(period), _started(crl::now()) {
 		}