diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index 67d205cd3..0d7b4575c 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -40,7 +40,9 @@ void SendExistingMedia(
 	message.action.generateLocal = true;
 	api->sendAction(message.action);
 
-	const auto newId = FullMsgId(peerToChannel(peer->id), clientMsgId());
+	const auto newId = FullMsgId(
+		peerToChannel(peer->id),
+		session->data().nextLocalMessageId());
 	const auto randomId = rand_value<uint64>();
 
 	auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
@@ -84,6 +86,13 @@ void SendExistingMedia(
 	const auto replyTo = message.action.replyTo;
 	const auto captionText = caption.text;
 
+	if (message.action.options.scheduled) {
+		flags |= MTPDmessage::Flag::f_from_scheduled;
+		sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
+	} else {
+		clientFlags |= MTPDmessage_ClientFlag::f_local_history_entry;
+	}
+
 	session->data().registerMessageRandomId(randomId, newId);
 
 	history->addNewLocalMessage(
@@ -92,7 +101,7 @@ void SendExistingMedia(
 		clientFlags,
 		0,
 		replyTo,
-		base::unixtime::now(),
+		HistoryItem::NewMessageDate(message.action.options.scheduled),
 		messageFromId,
 		messagePostAuthor,
 		media,
@@ -111,7 +120,7 @@ void SendExistingMedia(
 			MTP_long(randomId),
 			MTPReplyMarkup(),
 			sentEntities,
-			MTP_int(0) // schedule_date
+			MTP_int(message.action.options.scheduled)
 		)).done([=](const MTPUpdates &result) {
 			api->applyUpdates(result, randomId);
 		}).fail([=](const RPCError &error) {
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 0dbc89a99..4bae3e00b 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_media_types.h"
 #include "data/data_sparse_ids.h"
 #include "data/data_search_controller.h"
+#include "data/data_scheduled_messages.h"
 #include "data/data_channel_admins.h"
 #include "data/data_session.h"
 #include "data/data_channel.h"
@@ -4427,6 +4428,12 @@ void ApiWrap::forwardMessages(
 	if (silentPost) {
 		sendFlags |= MTPmessages_ForwardMessages::Flag::f_silent;
 	}
+	if (action.options.scheduled) {
+		flags |= MTPDmessage::Flag::f_from_scheduled;
+		sendFlags |= MTPmessages_ForwardMessages::Flag::f_schedule_date;
+	} else {
+		clientFlags |= MTPDmessage_ClientFlag::f_local_history_entry;
+	}
 
 	auto forwardFrom = items.front()->history()->peer;
 	auto currentGroupId = items.front()->groupId();
@@ -4448,7 +4455,7 @@ void ApiWrap::forwardMessages(
 			MTP_vector<MTPint>(ids),
 			MTP_vector<MTPlong>(randomIds),
 			peer->input,
-			MTP_int(0) // schedule_date
+			MTP_int(action.options.scheduled)
 		)).done([=, callback = std::move(successCallback)](
 				const MTPUpdates &updates) {
 			applyUpdates(updates);
@@ -4480,7 +4487,7 @@ void ApiWrap::forwardMessages(
 			if (const auto message = item->toHistoryMessage()) {
 				const auto newId = FullMsgId(
 					peerToChannel(peer->id),
-					clientMsgId());
+					session().data().nextLocalMessageId());
 				const auto self = _session->user();
 				const auto messageFromId = channelPost
 					? UserId(0)
@@ -4492,7 +4499,7 @@ void ApiWrap::forwardMessages(
 					newId.msg,
 					flags,
 					clientFlags,
-					base::unixtime::now(),
+					HistoryItem::NewMessageDate(action.options.scheduled),
 					messageFromId,
 					messagePostAuthor,
 					message);
@@ -4554,7 +4561,9 @@ void ApiWrap::sendSharedContact(
 	const auto history = action.history;
 	const auto peer = history->peer;
 
-	const auto newId = FullMsgId(history->channelId(), clientMsgId());
+	const auto newId = FullMsgId(
+		history->channelId(),
+		session().data().nextLocalMessageId());
 	const auto channelPost = peer->isChannel() && !peer->isMegagroup();
 
 	auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
@@ -4571,6 +4580,11 @@ void ApiWrap::sendSharedContact(
 	} else {
 		flags |= MTPDmessage::Flag::f_from_id;
 	}
+	if (action.options.scheduled) {
+		flags |= MTPDmessage::Flag::f_from_scheduled;
+	} else {
+		clientFlags |= MTPDmessage_ClientFlag::f_local_history_entry;
+	}
 	const auto messageFromId = channelPost ? 0 : _session->userId();
 	const auto messagePostAuthor = channelPost
 		? App::peerName(_session->user())
@@ -4586,7 +4600,7 @@ void ApiWrap::sendSharedContact(
 			MTPMessageFwdHeader(),
 			MTPint(),
 			MTP_int(action.replyTo),
-			MTP_int(base::unixtime::now()),
+			MTP_int(HistoryItem::NewMessageDate(action.options.scheduled)),
 			MTP_string(),
 			MTP_messageMediaContact(
 				MTP_string(phone),
@@ -4907,7 +4921,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 	HistoryItem *lastMessage = nullptr;
 
 	while (TextUtilities::CutPart(sending, left, MaxMessageSize)) {
-		auto newId = FullMsgId(peerToChannel(peer->id), clientMsgId());
+		auto newId = FullMsgId(
+			peerToChannel(peer->id),
+			session().data().nextLocalMessageId());
 		auto randomId = rand_value<uint64>();
 
 		TextUtilities::Trim(sending);
@@ -4963,6 +4979,12 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 		auto messagePostAuthor = channelPost
 			? App::peerName(_session->user())
 			: QString();
+		if (action.options.scheduled) {
+			flags |= MTPDmessage::Flag::f_from_scheduled;
+			sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date;
+		} else {
+			clientFlags |= MTPDmessage_ClientFlag::f_local_history_entry;
+		}
 		lastMessage = history->addNewMessage(
 			MTP_message(
 				MTP_flags(flags),
@@ -4972,7 +4994,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 				MTPMessageFwdHeader(),
 				MTPint(),
 				MTP_int(action.replyTo),
-				MTP_int(base::unixtime::now()),
+				MTP_int(
+					HistoryItem::NewMessageDate(action.options.scheduled)),
 				msgText,
 				media,
 				MTPReplyMarkup(),
@@ -4993,7 +5016,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 			MTP_long(randomId),
 			MTPReplyMarkup(),
 			sentEntities,
-			MTP_int(0) // schedule_date
+			MTP_int(action.options.scheduled)
 		)).done([=](const MTPUpdates &result) {
 			applyUpdates(result, randomId);
 			history->clearSentDraftText(QString());
@@ -5053,7 +5076,9 @@ void ApiWrap::sendInlineResult(
 
 	const auto history = action.history;
 	const auto peer = history->peer;
-	const auto newId = FullMsgId(peerToChannel(peer->id), clientMsgId());
+	const auto newId = FullMsgId(
+		peerToChannel(peer->id),
+		session().data().nextLocalMessageId());
 	const auto randomId = rand_value<uint64>();
 
 	auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
@@ -5081,14 +5106,17 @@ void ApiWrap::sendInlineResult(
 	if (bot) {
 		flags |= MTPDmessage::Flag::f_via_bot_id;
 	}
+	if (action.options.scheduled) {
+		flags |= MTPDmessage::Flag::f_from_scheduled;
+		sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_schedule_date;
+	} else {
+		clientFlags |= MTPDmessage_ClientFlag::f_local_history_entry;
+	}
 
-	auto messageFromId = channelPost ? 0 : _session->userId();
-	auto messagePostAuthor = channelPost
+	const auto messageFromId = channelPost ? 0 : _session->userId();
+	const auto messagePostAuthor = channelPost
 		? App::peerName(_session->user())
 		: QString();
-	MTPint messageDate = MTP_int(base::unixtime::now());
-	UserId messageViaBotId = bot ? peerToUser(bot->id) : 0;
-	MsgId messageId = newId.msg;
 
 	_session->data().registerMessageRandomId(randomId, newId);
 
@@ -5096,10 +5124,10 @@ void ApiWrap::sendInlineResult(
 		history,
 		flags,
 		clientFlags,
-		messageId,
+		newId.msg,
 		messageFromId,
-		messageDate,
-		messageViaBotId,
+		MTP_int(HistoryItem::NewMessageDate(action.options.scheduled)),
+		bot ? peerToUser(bot->id) : 0,
 		action.replyTo,
 		messagePostAuthor);
 
@@ -5113,7 +5141,7 @@ void ApiWrap::sendInlineResult(
 		MTP_long(randomId),
 		MTP_long(data->getQueryId()),
 		MTP_string(data->getId()),
-		MTP_int(0) // schedule_date
+		MTP_int(action.options.scheduled)
 	)).done([=](const MTPUpdates &result) {
 		applyUpdates(result, randomId);
 		history->clearSentDraftText(QString());
@@ -5236,6 +5264,9 @@ void ApiWrap::sendMediaWithRandomId(
 			: MTPmessages_SendMedia::Flag(0))
 		| (!sentEntities.v.isEmpty()
 			? MTPmessages_SendMedia::Flag::f_entities
+			: MTPmessages_SendMedia::Flag(0))
+		| (options.scheduled
+			? MTPmessages_SendMedia::Flag::f_schedule_date
 			: MTPmessages_SendMedia::Flag(0));
 
 	const auto peer = history->peer;
@@ -5249,7 +5280,7 @@ void ApiWrap::sendMediaWithRandomId(
 		MTP_long(randomId),
 		MTPReplyMarkup(),
 		sentEntities,
-		MTP_int(0) // schedule_date
+		MTP_int(options.scheduled)
 	)).done([=](const MTPUpdates &result) {
 		applyUpdates(result);
 	}).fail([=](const RPCError &error) {
@@ -5330,6 +5361,9 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
 			: MTPmessages_SendMultiMedia::Flag(0))
 		| (album->options.silent
 			? MTPmessages_SendMultiMedia::Flag::f_silent
+			: MTPmessages_SendMultiMedia::Flag(0))
+		| (album->options.scheduled
+			? MTPmessages_SendMultiMedia::Flag::f_schedule_date
 			: MTPmessages_SendMultiMedia::Flag(0));
 	const auto peer = history->peer;
 	history->sendRequestId = request(MTPmessages_SendMultiMedia(
@@ -5337,7 +5371,7 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
 		peer->input,
 		MTP_int(replyTo),
 		MTP_vector<MTPInputSingleMedia>(medias),
-		MTP_int(0) // schedule_date
+		MTP_int(album->options.scheduled)
 	)).done([=](const MTPUpdates &result) {
 		_sendingAlbums.remove(groupId);
 		applyUpdates(result);
@@ -5360,10 +5394,7 @@ FileLoadTo ApiWrap::fileLoadTaskOptions(const SendAction &action) const {
 	if (_session->data().notifySilentPosts(peer)) {
 		options.silent = true;
 	}
-	return FileLoadTo(
-		peer->id,
-		action.options,
-		action.replyTo);
+	return FileLoadTo(peer->id, action.options, action.replyTo);
 }
 
 void ApiWrap::requestSupportContact(FnMut<void(const MTPUser &)> callback) {
@@ -5387,7 +5418,9 @@ void ApiWrap::uploadPeerPhoto(not_null<PeerData*> peer, QImage &&image) {
 	peer = peer->migrateToOrMe();
 	const auto ready = PreparePeerPhoto(peer->id, std::move(image));
 
-	const auto fakeId = FullMsgId(peerToChannel(peer->id), clientMsgId());
+	const auto fakeId = FullMsgId(
+		peerToChannel(peer->id),
+		session().data().nextLocalMessageId());
 	const auto already = ranges::find(
 		_peerPhotoUploads,
 		peer,
@@ -5817,6 +5850,9 @@ void ApiWrap::createPoll(
 	if (silentPost) {
 		sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
 	}
+	if (action.options.scheduled) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
+	}
 
 	const auto replyTo = action.replyTo;
 	history->sendRequestId = request(MTPmessages_SendMedia(
@@ -5828,7 +5864,7 @@ void ApiWrap::createPoll(
 		MTP_long(rand_value<uint64>()),
 		MTPReplyMarkup(),
 		MTPVector<MTPMessageEntity>(),
-		MTP_int(0) // schedule_date
+		MTP_int(action.options.scheduled)
 	)).done([=, done = std::move(done)](const MTPUpdates &result) mutable {
 		applyUpdates(result);
 		done();
diff --git a/Telegram/SourceFiles/boxes/confirm_box.cpp b/Telegram/SourceFiles/boxes/confirm_box.cpp
index f83449b44..088c4034e 100644
--- a/Telegram/SourceFiles/boxes/confirm_box.cpp
+++ b/Telegram/SourceFiles/boxes/confirm_box.cpp
@@ -771,8 +771,14 @@ void DeleteMessagesBox::deleteAndClear() {
 		if (const auto item = _session->data().message(itemId)) {
 			const auto history = item->history();
 			if (item->isScheduled()) {
-				scheduledIdsByPeer[history->peer].push_back(MTP_int(
-					_session->data().scheduledMessages().lookupId(item)));
+				const auto wasOnServer = !item->isSending()
+					&& !item->hasFailed();
+				if (wasOnServer) {
+					scheduledIdsByPeer[history->peer].push_back(MTP_int(
+						_session->data().scheduledMessages().lookupId(item)));
+				} else {
+					_session->data().scheduledMessages().removeSending(item);
+				}
 				continue;
 			}
 			const auto wasOnServer = IsServerMsgId(item->id);
diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp
index 45dba1d27..1a38ade7d 100644
--- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp
+++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp
@@ -137,6 +137,49 @@ void ScheduledMessages::apply(
 	_updates.fire_copy(history);
 }
 
+void ScheduledMessages::apply(
+		const MTPDupdateMessageID &update,
+		not_null<HistoryItem*> local) {
+	const auto id = update.vid().v;
+	const auto i = _data.find(local->history());
+	Assert(i != end(_data));
+	auto &list = i->second;
+	const auto j = list.itemById.find(id);
+	if (j != end(list.itemById)) {
+		local->destroy();
+	} else {
+		Assert(!list.itemById.contains(local->id));
+		Assert(!list.idByItem.contains(local));
+		local->setRealId(local->history()->nextNonHistoryEntryId());
+		list.idByItem.emplace(local, id);
+		list.itemById.emplace(id, local);
+	}
+}
+
+void ScheduledMessages::appendSending(not_null<HistoryItem*> item) {
+	Expects(item->isSending());
+	Expects(item->isScheduled());
+
+	const auto history = item->history();
+	auto &list = _data[history];
+	list.items.emplace_back(item);
+	sort(list);
+	_updates.fire_copy(history);
+}
+
+void ScheduledMessages::removeSending(not_null<HistoryItem*> item) {
+	Expects(item->isSending() || item->hasFailed());
+	Expects(item->isScheduled());
+
+	const auto history = item->history();
+	auto &list = _data[history];
+	Assert(!list.itemById.contains(item->id));
+	Assert(!list.idByItem.contains(item));
+	list.items.erase(
+		ranges::remove(list.items, item.get(), &OwnedItem::get),
+		end(list.items));
+}
+
 rpl::producer<> ScheduledMessages::updates(not_null<History*> history) {
 	request(history);
 
@@ -175,10 +218,12 @@ void ScheduledMessages::request(not_null<History*> history) {
 	if (request.requestId) {
 		return;
 	}
+	const auto i = _data.find(history);
+	const auto hash = (i != end(_data)) ? countListHash(i->second) : 0;
 	request.requestId = _session->api().request(
 		MTPmessages_GetScheduledHistory(
 			history->peer->input,
-			MTP_int(request.hash))
+			MTP_int(hash))
 	).done([=](const MTPmessages_Messages &result) {
 		parse(history, result);
 	}).fail([=](const RPCError &error) {
@@ -197,14 +242,33 @@ void ScheduledMessages::parse(
 	}, [&](const auto &data) {
 		_session->data().processUsers(data.vusers());
 		_session->data().processChats(data.vchats());
+
 		const auto &messages = data.vmessages().v;
 		if (messages.isEmpty()) {
+			if (element != end(_data)) {
+				_data.erase(element);
+				element = end(_data);
+				_updates.fire_copy(history);
+			}
 			return;
 		}
 		element = _data.emplace(history, List()).first;
+		auto received = base::flat_set<not_null<HistoryItem*>>();
 		auto &list = element->second;
 		for (const auto &message : messages) {
-			append(history, list, message);
+			if (const auto item = append(history, list, message)) {
+				received.emplace(item);
+			}
+		}
+		auto clear = base::flat_set<not_null<HistoryItem*>>();
+		for (const auto &owned : list.items) {
+			const auto item = owned.get();
+			if (!item->isSending() && !received.contains(item)) {
+				clear.emplace(item);
+			}
+		}
+		for (const auto item : clear) {
+			item->destroy();
 		}
 		if (!list.items.empty()) {
 			sort(list);
@@ -214,24 +278,21 @@ void ScheduledMessages::parse(
 		}
 		_updates.fire_copy(history);
 	});
-
-	request.hash = (element != end(_data))
-		? countListHash(element->second)
-		: 0;
-	if (!request.requestId && !request.hash) {
+	if (!request.requestId) {
 		_requests.remove(history);
 	}
 }
 
-void ScheduledMessages::append(
+HistoryItem *ScheduledMessages::append(
 		not_null<History*> history,
 		List &list,
 		const MTPMessage &message) {
 	const auto id = message.match([&](const auto &data) {
 		return data.vid().v;
 	});
-	if (list.itemById.find(id) != end(list.itemById)) {
-		return;
+	const auto i = list.itemById.find(id);
+	if (i != end(list.itemById)) {
+		return i->second;
 	}
 
 	const auto item = _session->data().addNewMessage(
@@ -240,11 +301,12 @@ void ScheduledMessages::append(
 		NewMessageType::Existing);
 	if (!item || item->history() != history) {
 		LOG(("API Error: Bad data received in scheduled messages."));
-		return;
+		return nullptr;
 	}
 	list.items.emplace_back(item);
 	list.itemById.emplace(id, item);
 	list.idByItem.emplace(item, id);
+	return item;
 }
 
 void ScheduledMessages::sort(List &list) {
@@ -277,7 +339,12 @@ int32 ScheduledMessages::countListHash(const List &list) const {
 	using namespace Api;
 
 	auto hash = HashInit();
-	for (const auto &item : list.items | ranges::view::reverse) {
+	auto &&serverside = ranges::view::all(
+		list.items
+	) | ranges::view::filter([](const OwnedItem &item) {
+		return !item->isSending() && !item->hasFailed();
+	}) | ranges::view::reverse;
+	for (const auto &item : serverside) {
 		const auto j = list.idByItem.find(item.get());
 		HashUpdate(hash, j->second);
 		if (const auto edited = item->Get<HistoryMessageEdited>()) {
diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.h b/Telegram/SourceFiles/data/data_scheduled_messages.h
index 53ba5e80d..d29189e42 100644
--- a/Telegram/SourceFiles/data/data_scheduled_messages.h
+++ b/Telegram/SourceFiles/data/data_scheduled_messages.h
@@ -32,6 +32,12 @@ public:
 
 	void apply(const MTPDupdateNewScheduledMessage &update);
 	void apply(const MTPDupdateDeleteScheduledMessages &update);
+	void apply(
+		const MTPDupdateMessageID &update,
+		not_null<HistoryItem*> local);
+
+	void appendSending(not_null<HistoryItem*> item);
+	void removeSending(not_null<HistoryItem*> item);
 
 	[[nodiscard]] rpl::producer<> updates(not_null<History*> history);
 	[[nodiscard]] Data::MessagesSlice list(not_null<History*> history);
@@ -44,7 +50,6 @@ private:
 		base::flat_map<not_null<HistoryItem*>, MsgId> idByItem;
 	};
 	struct Request {
-		int32 hash = 0;
 		mtpRequestId requestId = 0;
 	};
 
@@ -52,7 +57,7 @@ private:
 	void parse(
 		not_null<History*> history,
 		const MTPmessages_Messages &list);
-	void append(
+	HistoryItem *append(
 		not_null<History*> history,
 		List &list,
 		const MTPMessage &message);
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 899099fee..b650abf8b 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -1844,6 +1844,12 @@ void Session::destroyMessage(not_null<HistoryItem*> item) {
 	list->erase(item->id);
 }
 
+MsgId Session::nextLocalMessageId() {
+	Expects(_localMessageIdCounter < EndClientMsgId);
+
+	return _localMessageIdCounter++;
+}
+
 HistoryItem *Session::message(ChannelId channelId, MsgId itemId) const {
 	if (!itemId) {
 		return nullptr;
@@ -3627,13 +3633,14 @@ void Session::insertCheckedServiceNotification(
 	const auto flags = MTPDmessage::Flag::f_entities
 		| MTPDmessage::Flag::f_from_id
 		| MTPDmessage::Flag::f_media;
-	const auto clientFlags = MTPDmessage_ClientFlag::f_clientside_unread;
+	const auto clientFlags = MTPDmessage_ClientFlag::f_clientside_unread
+		| MTPDmessage_ClientFlag::f_local_history_entry;
 	auto sending = TextWithEntities(), left = message;
 	while (TextUtilities::CutPart(sending, left, MaxMessageSize)) {
 		addNewMessage(
 			MTP_message(
 				MTP_flags(flags),
-				MTP_int(clientMsgId()),
+				MTP_int(nextLocalMessageId()),
 				MTP_int(peerToUser(PeerData::kServiceNotificationsId)),
 				MTP_peerUser(MTP_int(_session->userId())),
 				MTPMessageFwdHeader(),
diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h
index ca9d4c3f3..0fe5b4c7f 100644
--- a/Telegram/SourceFiles/data/data_session.h
+++ b/Telegram/SourceFiles/data/data_session.h
@@ -387,6 +387,7 @@ public:
 		ChannelId channelId,
 		const QVector<MTPint> &data);
 
+	[[nodiscard]] MsgId nextLocalMessageId();
 	[[nodiscard]] HistoryItem *message(
 		ChannelId channelId,
 		MsgId itemId) const;
@@ -877,6 +878,7 @@ private:
 	Dialogs::IndexedList _contactsList;
 	Dialogs::IndexedList _contactsNoChatsList;
 
+	MsgId _localMessageIdCounter = StartClientMsgId;
 	Messages _messages;
 	std::map<ChannelId, Messages> _channelMessages;
 	std::map<
diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h
index 04f5e887c..2b452b3e4 100644
--- a/Telegram/SourceFiles/data/data_types.h
+++ b/Telegram/SourceFiles/data/data_types.h
@@ -415,12 +415,6 @@ inline bool operator!=(const AudioMsgId &a, const AudioMsgId &b) {
 	return !(a == b);
 }
 
-inline MsgId clientMsgId() {
-	static MsgId CurrentClientMsgId = StartClientMsgId;
-	Assert(CurrentClientMsgId < EndClientMsgId);
-	return CurrentClientMsgId++;
-}
-
 struct MessageCursor {
 	MessageCursor() = default;
 	MessageCursor(int position, int anchor, int scroll)
diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index f40d83fc5..c177d5c65 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -16,6 +16,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_drafts.h"
 #include "data/data_session.h"
 #include "data/data_media_types.h"
+#include "data/data_channel_admins.h"
+#include "data/data_scheduled_messages.h"
+#include "data/data_folder.h"
+#include "data/data_photo.h"
+#include "data/data_channel.h"
+#include "data/data_chat.h"
+#include "data/data_user.h"
 #include "lang/lang_keys.h"
 #include "apiwrap.h"
 #include "mainwidget.h"
@@ -29,12 +36,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "storage/storage_shared_media.h"
 //#include "storage/storage_feed_messages.h" // #feed
 #include "support/support_helper.h"
-#include "data/data_channel_admins.h"
-#include "data/data_folder.h"
-#include "data/data_photo.h"
-#include "data/data_channel.h"
-#include "data/data_chat.h"
-#include "data/data_user.h"
 #include "ui/image/image.h"
 #include "ui/text_options.h"
 #include "core/crash_reports.h"
@@ -622,51 +623,50 @@ HistoryItem *History::addNewMessage(
 		const MTPMessage &msg,
 		MTPDmessage_ClientFlags clientFlags,
 		NewMessageType type) {
-	if (type == NewMessageType::Existing) {
-		return addToHistory(msg, clientFlags);
-	}
-	if (!loadedAtBottom() || peer->migrateTo()) {
-		if (const auto item = addToHistory(msg, clientFlags)) {
-			setLastMessage(item);
-			if (type == NewMessageType::Unread) {
-				newItemAdded(item);
-			}
-			return item;
-		}
+	const auto detachExistingItem = (type == NewMessageType::Unread);
+	const auto item = createItem(msg, clientFlags, detachExistingItem);
+	if (!item) {
 		return nullptr;
 	}
-
-	return addNewToLastBlock(msg, clientFlags, type);
-}
-
-HistoryItem *History::addNewToLastBlock(
-		const MTPMessage &msg,
-		MTPDmessage_ClientFlags clientFlags,
-		NewMessageType type) {
-	Expects(type != NewMessageType::Existing);
-
-	const auto detachExistingItem = (type != NewMessageType::Last);
-	const auto item = createItem(msg, clientFlags, detachExistingItem);
-	if (!item || item->mainView()) {
+	if (type == NewMessageType::Existing || item->mainView()) {
 		return item;
 	}
-	const auto newUnreadMessage = (type == NewMessageType::Unread);
-	if (newUnreadMessage) {
+	const auto unread = (type == NewMessageType::Unread);
+	if (unread && item->isHistoryEntry()) {
 		applyMessageChanges(item, msg);
 	}
-	const auto result = addNewItem(item, newUnreadMessage);
-	checkForLoadedAtTop(result);
-	if (type == NewMessageType::Last) {
-		// When we add just one last item, like we do while loading dialogs,
-		// we want to remove a single added grouped media, otherwise it will
-		// jump once we open the message history (first we show only that
-		// media, then we load the rest of the group and show the group).
-		//
-		// That way when we open the message history we show nothing until a
-		// whole history part is loaded, it certainly will contain the group.
-		removeOrphanMediaGroupPart();
+	return addNewItem(item, unread);
+}
+
+not_null<HistoryItem*> History::addNewItem(
+		not_null<HistoryItem*> item,
+		bool unread) {
+	if (item->isScheduled()) {
+		session().data().scheduledMessages().appendSending(item);
+		return item;
+	} else if (!item->isHistoryEntry()) {
+		return item;
 	}
-	return result;
+	if (!loadedAtBottom() || peer->migrateTo()) {
+		setLastMessage(item);
+		if (unread) {
+			newItemAdded(item);
+		}
+	} else {
+		addNewToBack(item, unread);
+		checkForLoadedAtTop(item);
+		if (!unread) {
+			// When we add just one last item, like we do while loading dialogs,
+			// we want to remove a single added grouped media, otherwise it will
+			// jump once we open the message history (first we show only that
+			// media, then we load the rest of the group and show the group).
+			//
+			// That way when we open the message history we show nothing until a
+			// whole history part is loaded, it certainly will contain the group.
+			removeOrphanMediaGroupPart();
+		}
+	}
+	return item;
 }
 
 void History::checkForLoadedAtTop(not_null<HistoryItem*> added) {
@@ -896,7 +896,7 @@ void History::addUnreadMentionsSlice(const MTPmessages_Messages &result) {
 	Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::UnreadMentionsChanged);
 }
 
-not_null<HistoryItem*> History::addNewItem(
+not_null<HistoryItem*> History::addNewToBack(
 		not_null<HistoryItem*> item,
 		bool unread) {
 	Expects(!isBuildingFrontBlock());
@@ -2866,7 +2866,7 @@ void History::insertLocalMessage(not_null<HistoryItem*> item) {
 	Expects(item->mainView() == nullptr);
 
 	if (isEmpty()) {
-		addNewItem(item, false);
+		addNewToBack(item, false);
 		return;
 	}
 
diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h
index f881b0add..48db8369e 100644
--- a/Telegram/SourceFiles/history/history.h
+++ b/Telegram/SourceFiles/history/history.h
@@ -387,11 +387,6 @@ private:
 	// helper method for countScrollState(int top)
 	void countScrollTopItem(int top);
 
-	HistoryItem *addNewToLastBlock(
-		const MTPMessage &msg,
-		MTPDmessage_ClientFlags clientFlags,
-		NewMessageType type);
-
 	// this method just removes a block from the blocks list
 	// when the last item from this block was detached and
 	// calls the required previousItemChanged()
@@ -401,6 +396,9 @@ private:
 	not_null<HistoryItem*> addNewItem(
 		not_null<HistoryItem*> item,
 		bool unread);
+	not_null<HistoryItem*> addNewToBack(
+		not_null<HistoryItem*> item,
+		bool unread);
 	not_null<HistoryItem*> addNewInTheMiddle(
 		not_null<HistoryItem*> item,
 		int blockIndex,
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index fe3ba57b9..07082e6f9 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -187,6 +187,11 @@ TimeId HistoryItem::date() const {
 	return _date;
 }
 
+TimeId HistoryItem::NewMessageDate(TimeId scheduled) {
+	const auto now = base::unixtime::now();
+	return scheduled ? std::max(scheduled, now + 60) : now;
+}
+
 void HistoryItem::finishEdition(int oldKeyboardTop) {
 	_history->owner().requestItemViewRefresh(this);
 	invalidateChatListEntry();
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index 6f95b9767..60d66dec8 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -86,7 +86,8 @@ public:
 	UserData *getMessageBot() const;
 
 	[[nodiscard]] bool isHistoryEntry() const {
-		return (id < ServerMaxMsgId);
+		return IsServerMsgId(id)
+			|| (_clientFlags & MTPDmessage_ClientFlag::f_local_history_entry);
 	}
 	[[nodiscard]] bool isFromScheduled() const {
 		return isHistoryEntry()
@@ -190,13 +191,13 @@ public:
 		return _clientFlags & MTPDmessage_ClientFlag::f_failed;
 	}
 	void sendFailed();
-	virtual int viewsCount() const {
+	[[nodiscard]] virtual int viewsCount() const {
 		return hasViews() ? 1 : -1;
 	}
 
-	virtual bool needCheck() const;
+	[[nodiscard]] virtual bool needCheck() const;
 
-	virtual bool serviceMsg() const {
+	[[nodiscard]] virtual bool serviceMsg() const {
 		return false;
 	}
 	virtual void applyEdition(const MTPDmessage &message) {
@@ -218,14 +219,14 @@ public:
 	virtual void addToUnreadMentions(UnreadMentionType type);
 	virtual void eraseFromUnreadMentions() {
 	}
-	virtual Storage::SharedMediaTypesMask sharedMediaTypes() const = 0;
+	[[nodiscard]] virtual Storage::SharedMediaTypesMask sharedMediaTypes() const = 0;
 
 	void indexAsNewItem();
 
-	virtual QString notificationHeader() const {
+	[[nodiscard]] virtual QString notificationHeader() const {
 		return QString();
 	}
-	virtual QString notificationText() const;
+	[[nodiscard]] virtual QString notificationText() const;
 
 	enum class DrawInDialog {
 		Normal,
@@ -234,15 +235,15 @@ public:
 
 	// Returns text with link-start and link-end commands for service-color highlighting.
 	// Example: "[link1-start]You:[link1-end] [link1-start]Photo,[link1-end] caption text"
-	virtual QString inDialogsText(DrawInDialog way) const;
-	virtual QString inReplyText() const {
+	[[nodiscard]] virtual QString inDialogsText(DrawInDialog way) const;
+	[[nodiscard]] virtual QString inReplyText() const {
 		return inDialogsText(DrawInDialog::WithoutSender);
 	}
-	virtual Ui::Text::IsolatedEmoji isolatedEmoji() const;
-	virtual TextWithEntities originalText() const {
+	[[nodiscard]] virtual Ui::Text::IsolatedEmoji isolatedEmoji() const;
+	[[nodiscard]] virtual TextWithEntities originalText() const {
 		return TextWithEntities();
 	}
-	virtual TextForMimeData clipboardText() const {
+	[[nodiscard]] virtual TextForMimeData clipboardText() const {
 		return TextForMimeData();
 	}
 
@@ -259,81 +260,83 @@ public:
 		const HistoryItem *&cacheFor,
 		Ui::Text::String &cache) const;
 
-	bool emptyText() const {
+	[[nodiscard]] bool emptyText() const {
 		return _text.isEmpty();
 	}
 
-	bool isPinned() const;
-	bool canPin() const;
-	bool canStopPoll() const;
-	virtual bool allowsSendNow() const;
-	virtual bool allowsForward() const;
-	virtual bool allowsEdit(TimeId now) const;
-	bool canDelete() const;
-	bool canDeleteForEveryone(TimeId now) const;
-	bool suggestReport() const;
-	bool suggestBanReport() const;
-	bool suggestDeleteAllReport() const;
+	[[nodiscard]] bool isPinned() const;
+	[[nodiscard]] bool canPin() const;
+	[[nodiscard]] bool canStopPoll() const;
+	[[nodiscard]] virtual bool allowsSendNow() const;
+	[[nodiscard]] virtual bool allowsForward() const;
+	[[nodiscard]] virtual bool allowsEdit(TimeId now) const;
+	[[nodiscard]] bool canDelete() const;
+	[[nodiscard]] bool canDeleteForEveryone(TimeId now) const;
+	[[nodiscard]] bool suggestReport() const;
+	[[nodiscard]] bool suggestBanReport() const;
+	[[nodiscard]] bool suggestDeleteAllReport() const;
 
-	bool hasDirectLink() const;
+	[[nodiscard]] bool hasDirectLink() const;
 
-	MsgId id;
-
-	ChannelId channelId() const;
-	FullMsgId fullId() const {
+	[[nodiscard]] ChannelId channelId() const;
+	[[nodiscard]] FullMsgId fullId() const {
 		return FullMsgId(channelId(), id);
 	}
-	Data::MessagePosition position() const;
-	TimeId date() const;
+	[[nodiscard]] Data::MessagePosition position() const;
+	[[nodiscard]] TimeId date() const;
 
-	Data::Media *media() const {
+	[[nodiscard]] static TimeId NewMessageDate(TimeId scheduled);
+
+	[[nodiscard]] Data::Media *media() const {
 		return _media.get();
 	}
 	virtual void setText(const TextWithEntities &textWithEntities) {
 	}
-	virtual bool textHasLinks() const {
+	[[nodiscard]] virtual bool textHasLinks() const {
 		return false;
 	}
 
-	virtual HistoryMessage *toHistoryMessage() { // dynamic_cast optimize
+	[[nodiscard]] virtual HistoryMessage *toHistoryMessage() { // dynamic_cast optimize
 		return nullptr;
 	}
-	virtual const HistoryMessage *toHistoryMessage() const { // dynamic_cast optimize
+	[[nodiscard]] virtual const HistoryMessage *toHistoryMessage() const { // dynamic_cast optimize
 		return nullptr;
 	}
-	MsgId replyToId() const;
+	[[nodiscard]] MsgId replyToId() const;
 
-	not_null<PeerData*> author() const;
+	[[nodiscard]] not_null<PeerData*> author() const;
 
-	TimeId dateOriginal() const;
-	PeerData *senderOriginal() const;
-	const HiddenSenderInfo *hiddenForwardedInfo() const;
-	not_null<PeerData*> fromOriginal() const;
-	QString authorOriginal() const;
-	MsgId idOriginal() const;
+	[[nodiscard]] TimeId dateOriginal() const;
+	[[nodiscard]] PeerData *senderOriginal() const;
+	[[nodiscard]] const HiddenSenderInfo *hiddenForwardedInfo() const;
+	[[nodiscard]] not_null<PeerData*> fromOriginal() const;
+	[[nodiscard]] QString authorOriginal() const;
+	[[nodiscard]] MsgId idOriginal() const;
 
-	bool isEmpty() const;
+	[[nodiscard]] bool isEmpty() const;
 
-	MessageGroupId groupId() const;
+	[[nodiscard]] MessageGroupId groupId() const;
 
-	const HistoryMessageReplyMarkup *inlineReplyMarkup() const {
+	[[nodiscard]] const HistoryMessageReplyMarkup *inlineReplyMarkup() const {
 		return const_cast<HistoryItem*>(this)->inlineReplyMarkup();
 	}
-	const ReplyKeyboard *inlineReplyKeyboard() const {
+	[[nodiscard]] const ReplyKeyboard *inlineReplyKeyboard() const {
 		return const_cast<HistoryItem*>(this)->inlineReplyKeyboard();
 	}
-	HistoryMessageReplyMarkup *inlineReplyMarkup();
-	ReplyKeyboard *inlineReplyKeyboard();
+	[[nodiscard]] HistoryMessageReplyMarkup *inlineReplyMarkup();
+	[[nodiscard]] ReplyKeyboard *inlineReplyKeyboard();
 
 	[[nodiscard]] ChannelData *discussionPostOriginalSender() const;
 	[[nodiscard]] bool isDiscussionPost() const;
 	[[nodiscard]] PeerData *displayFrom() const;
 
-	virtual std::unique_ptr<HistoryView::Element> createView(
+	[[nodiscard]] virtual std::unique_ptr<HistoryView::Element> createView(
 		not_null<HistoryView::ElementDelegate*> delegate) = 0;
 
 	virtual ~HistoryItem();
 
+	MsgId id;
+
 protected:
 	HistoryItem(
 		not_null<History*> history,
diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp
index ce81f0b66..bd1891114 100644
--- a/Telegram/SourceFiles/history/history_service.cpp
+++ b/Telegram/SourceFiles/history/history_service.cpp
@@ -799,8 +799,8 @@ not_null<HistoryService*> GenerateJoinedMessage(
 		MTPDmessage::Flags flags) {
 	return new HistoryService(
 		history,
-		MTPDmessage_ClientFlags(),
-		clientMsgId(),
+		MTPDmessage_ClientFlag::f_local_history_entry,
+		history->owner().nextLocalMessageId(),
 		inviteDate,
 		GenerateJoinedText(history, inviter),
 		flags);
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 3aad4a198..d0d8c2752 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -634,12 +634,19 @@ HistoryWidget::HistoryWidget(
 	) | rpl::filter([=](const Api::SendAction &action) {
 		return (action.history == _history);
 	}) | rpl::start_with_next([=](const Api::SendAction &action) {
-		fastShowAtEnd(action.history);
-		const auto lastKeyboardUsed = lastForceReplyReplied(FullMsgId(
-			action.history->channelId(),
-			action.replyTo));
-		if (cancelReply(lastKeyboardUsed) && !action.clearDraft) {
-			onCloudDraftSave();
+		if (action.options.scheduled) {
+			crl::on_main(this, [=, history = action.history]{
+				controller->showSection(
+					HistoryView::ScheduledMemento(action.history));
+			});
+		} else {
+			fastShowAtEnd(action.history);
+			const auto lastKeyboardUsed = lastForceReplyReplied(FullMsgId(
+				action.history->channelId(),
+				action.replyTo));
+			if (cancelReply(lastKeyboardUsed) && !action.clearDraft) {
+				onCloudDraftSave();
+			}
 		}
 		if (action.options.handleSupportSwitch) {
 			handleSupportSwitch(action.history);
@@ -2953,7 +2960,7 @@ void HistoryWidget::sendSilent() {
 
 void HistoryWidget::sendScheduled() {
 	auto options = Api::SendOptions();
-	options.scheduled = INT_MAX;
+	options.scheduled = base::unixtime::now() + 86400;
 	send(options);
 }
 
@@ -4400,7 +4407,8 @@ void HistoryWidget::sendFileConfirmed(
 		channelId,
 		file->to.replyTo));
 
-	const auto newId = oldId.value_or(FullMsgId(channelId, clientMsgId()));
+	const auto newId = oldId.value_or(
+		FullMsgId(channelId, session().data().nextLocalMessageId()));
 	auto groupId = file->album ? file->album->groupId : uint64(0);
 	if (file->album) {
 		const auto proj = [](const SendingAlbum::Item &item) {
@@ -4423,6 +4431,7 @@ void HistoryWidget::sendFileConfirmed(
 	const auto peer = history->peer;
 
 	auto action = Api::SendAction(history);
+	action.options = file->to.options;
 	action.clearDraft = false;
 	action.replyTo = file->to.replyTo;
 	action.generateLocal = true;
@@ -4469,6 +4478,12 @@ void HistoryWidget::sendFileConfirmed(
 	if (groupId) {
 		flags |= MTPDmessage::Flag::f_grouped_id;
 	}
+	if (file->to.options.scheduled) {
+		flags |= MTPDmessage::Flag::f_from_scheduled;
+	} else {
+		clientFlags |= MTPDmessage_ClientFlag::f_local_history_entry;
+	}
+
 	const auto messageFromId = channelPost ? 0 : session().userId();
 	const auto messagePostAuthor = channelPost
 		? App::peerName(session().user())
@@ -4489,7 +4504,7 @@ void HistoryWidget::sendFileConfirmed(
 			MTPMessageFwdHeader(),
 			MTPint(),
 			MTP_int(file->to.replyTo),
-			MTP_int(base::unixtime::now()),
+			MTP_int(HistoryItem::NewMessageDate(file->to.options.scheduled)),
 			MTP_string(caption.text),
 			photo,
 			MTPReplyMarkup(),
@@ -4525,7 +4540,7 @@ void HistoryWidget::sendFileConfirmed(
 			MTPMessageFwdHeader(),
 			MTPint(),
 			MTP_int(file->to.replyTo),
-			MTP_int(base::unixtime::now()),
+			MTP_int(HistoryItem::NewMessageDate(file->to.options.scheduled)),
 			MTP_string(caption.text),
 			document,
 			MTPReplyMarkup(),
@@ -4564,7 +4579,8 @@ void HistoryWidget::sendFileConfirmed(
 				MTPMessageFwdHeader(),
 				MTPint(),
 				MTP_int(file->to.replyTo),
-				MTP_int(base::unixtime::now()),
+				MTP_int(
+					HistoryItem::NewMessageDate(file->to.options.scheduled)),
 				MTP_string(caption.text),
 				document,
 				MTPReplyMarkup(),
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index d75505f0b..b8b2290e1 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -3899,19 +3899,25 @@ void MainWidget::feedUpdate(const MTPUpdate &update) {
 		const auto &d = update.c_updateMessageID();
 		const auto randomId = d.vrandom_id().v;
 		if (const auto id = session().data().messageIdByRandomId(randomId)) {
-			const auto channel = id.channel;
 			const auto newId = d.vid().v;
 			if (const auto local = session().data().message(id)) {
-				const auto existing = session().data().message(channel, newId);
-				if (existing && !local->mainView()) {
-					const auto history = local->history();
-					local->destroy();
-					history->requestChatListMessage();
+				if (local->isScheduled()) {
+					session().data().scheduledMessages().apply(d, local);
 				} else {
-					if (existing) {
-						existing->destroy();
+					const auto channel = id.channel;
+					const auto existing = session().data().message(
+						channel,
+						newId);
+					if (existing && !local->mainView()) {
+						const auto history = local->history();
+						local->destroy();
+						history->requestChatListMessage();
+					} else {
+						if (existing) {
+							existing->destroy();
+						}
+						local->setRealId(d.vid().v);
 					}
-					local->setRealId(d.vid().v);
 				}
 			}
 			session().data().unregisterMessageRandomId(randomId);
diff --git a/Telegram/SourceFiles/mtproto/type_utils.h b/Telegram/SourceFiles/mtproto/type_utils.h
index 228a3f13c..ccaab5d3c 100644
--- a/Telegram/SourceFiles/mtproto/type_utils.h
+++ b/Telegram/SourceFiles/mtproto/type_utils.h
@@ -66,8 +66,8 @@ enum class MTPDmessage_ClientFlag : uint32 {
 	// message has no media and only a several emoji text
 	f_isolated_emoji = (1U << 21),
 
-	// update this when adding new client side flags
-	MIN_FIELD = (1U << 21),
+	// message is local message existing in the message history
+	f_local_history_entry = (1U << 20),
 };
 inline constexpr bool is_flag_type(MTPDmessage_ClientFlag) { return true; }
 using MTPDmessage_ClientFlags = base::flags<MTPDmessage_ClientFlag>;
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp
index 6670c8a29..c24813c2b 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp
@@ -1520,7 +1520,9 @@ void FormController::uploadEncryptedFile(
 	prepared->setFileData(prepared->content);
 	prepared->filemd5 = file.uploadData->md5checksum;
 
-	file.uploadData->fullId = FullMsgId(0, clientMsgId());
+	file.uploadData->fullId = FullMsgId(
+		0,
+		Auth().data().nextLocalMessageId());
 	Auth().uploader().upload(file.uploadData->fullId, std::move(prepared));
 }
 
diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp
index 21451d8b8..daf581689 100644
--- a/Telegram/SourceFiles/window/themes/window_theme.cpp
+++ b/Telegram/SourceFiles/window/themes/window_theme.cpp
@@ -429,7 +429,7 @@ void ChatBackground::checkUploadWallPaper() {
 
 	const auto ready = PrepareWallPaper(_original);
 	const auto documentId = ready.id;
-	_wallPaperUploadId = FullMsgId(0, clientMsgId());
+	_wallPaperUploadId = FullMsgId(0, _session->data().nextLocalMessageId());
 	_session->uploader().uploadMedia(_wallPaperUploadId, ready);
 	if (_wallPaperUploadLifetime) {
 		return;