diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index aa7e148f4..7b058e531 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -33,6 +33,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "mainwidget.h"
 #include "boxes/add_contact_box.h"
 #include "history/history_message.h"
+#include "history/history_media_types.h"
 #include "history/history_item_components.h"
 #include "storage/localstorage.h"
 #include "auth_session.h"
@@ -41,9 +42,11 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "window/notifications_manager.h"
 #include "chat_helpers/message_field.h"
 #include "chat_helpers/stickers.h"
+#include "storage/localimageloader.h"
 #include "storage/storage_facade.h"
 #include "storage/storage_shared_media.h"
 #include "storage/storage_user_photos.h"
+#include "storage/storage_media_prepare.h"
 #include "data/data_sparse_ids.h"
 #include "data/data_search_controller.h"
 #include "data/data_channel_admins.h"
@@ -59,6 +62,76 @@ constexpr auto kUnreadMentionsFirstRequestLimit = 10;
 constexpr auto kUnreadMentionsNextRequestLimit = 100;
 constexpr auto kSharedMediaLimit = 100;
 constexpr auto kReadFeaturedSetsTimeout = TimeMs(1000);
+constexpr auto kFileLoaderQueueStopTimeout = TimeMs(5000);
+
+bool IsSilentPost(not_null<HistoryItem*> item, bool silent) {
+	const auto history = item->history();
+	return silent
+		&& history->peer->isChannel()
+		&& !history->peer->isMegagroup();
+}
+
+MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
+		not_null<DocumentData*> document) {
+	const auto filenameAttribute = MTP_documentAttributeFilename(
+		MTP_string(document->filename()));
+	const auto dimensions = document->dimensions;
+	auto attributes = QVector<MTPDocumentAttribute>(1, filenameAttribute);
+	if (dimensions.width() > 0 && dimensions.height() > 0) {
+		const auto duration = document->duration();
+		if (duration >= 0) {
+			auto flags = MTPDdocumentAttributeVideo::Flags(0);
+			if (document->isVideoMessage()) {
+				flags |= MTPDdocumentAttributeVideo::Flag::f_round_message;
+			}
+			attributes.push_back(MTP_documentAttributeVideo(
+				MTP_flags(flags),
+				MTP_int(duration),
+				MTP_int(dimensions.width()),
+				MTP_int(dimensions.height())));
+		} else {
+			attributes.push_back(MTP_documentAttributeImageSize(
+				MTP_int(dimensions.width()),
+				MTP_int(dimensions.height())));
+		}
+	}
+	if (document->type == AnimatedDocument) {
+		attributes.push_back(MTP_documentAttributeAnimated());
+	} else if (document->type == StickerDocument && document->sticker()) {
+		attributes.push_back(MTP_documentAttributeSticker(
+			MTP_flags(0),
+			MTP_string(document->sticker()->alt),
+			document->sticker()->set,
+			MTPMaskCoords()));
+	} else if (const auto song = document->song()) {
+		const auto flags = MTPDdocumentAttributeAudio::Flag::f_title
+			| MTPDdocumentAttributeAudio::Flag::f_performer;
+		attributes.push_back(MTP_documentAttributeAudio(
+			MTP_flags(flags),
+			MTP_int(song->duration),
+			MTP_string(song->title),
+			MTP_string(song->performer),
+			MTPstring()));
+	} else if (const auto voice = document->voice()) {
+		const auto flags = MTPDdocumentAttributeAudio::Flag::f_voice
+			| MTPDdocumentAttributeAudio::Flag::f_waveform;
+		attributes.push_back(MTP_documentAttributeAudio(
+			MTP_flags(flags),
+			MTP_int(voice->duration),
+			MTPstring(),
+			MTPstring(),
+			MTP_bytes(documentWaveformEncode5bit(voice->waveform))));
+	}
+	return MTP_vector<MTPDocumentAttribute>(attributes);
+}
+
+FileLoadTo FileLoadTaskOptions(const ApiWrap::SendOptions &options) {
+	const auto peer = options.history->peer;
+	return FileLoadTo(
+		peer->id,
+		peer->notifySilentPosts(),
+		options.replyTo);
+}
 
 } // namespace
 
@@ -67,7 +140,8 @@ ApiWrap::ApiWrap(not_null<AuthSession*> session)
 , _messageDataResolveDelayed([this] { resolveMessageDatas(); })
 , _webPagesTimer([this] { resolveWebPages(); })
 , _draftsSaveTimer([this] { saveDraftsToCloud(); })
-, _featuredSetsReadTimer([this] { readFeaturedSets(); }) {
+, _featuredSetsReadTimer([this] { readFeaturedSets(); })
+, _fileLoader(std::make_unique<TaskQueue>(kFileLoaderQueueStopTimeout)) {
 }
 
 void ApiWrap::start() {
@@ -129,10 +203,26 @@ void ApiWrap::addLocalChangelogs(int oldAppVersion) {
 	}
 }
 
-void ApiWrap::applyUpdates(const MTPUpdates &updates, uint64 sentMessageRandomId) {
+void ApiWrap::applyUpdates(
+		const MTPUpdates &updates,
+		uint64 sentMessageRandomId) {
 	App::main()->feedUpdates(updates, sentMessageRandomId);
 }
 
+void ApiWrap::sendMessageFail(const RPCError &error) {
+	if (error.type() == qstr("PEER_FLOOD")) {
+		Ui::show(Box<InformBox>(
+			PeerFloodErrorText(PeerFloodType::Send)));
+	} else if (error.type() == qstr("USER_BANNED_IN_CHANNEL")) {
+		const auto link = textcmdLink(
+			Messenger::Instance().createInternalLinkFull(qsl("spambot")),
+			lang(lng_cant_more_info));
+		Ui::show(Box<InformBox>(lng_error_public_groups_denied(
+			lt_more_info,
+			link)));
+	}
+}
+
 void ApiWrap::requestMessageData(ChannelData *channel, MsgId msgId, RequestMessageDataCallback callback) {
 	auto &req = (channel ? _channelMessageDataRequests[channel][msgId] : _messageDataRequests[msgId]);
 	if (callback) {
@@ -2567,17 +2657,12 @@ void ApiWrap::sendSharedContact(
 	const auto history = options.history;
 	const auto peer = history->peer;
 
-	const auto randomId = rand_value<uint64>();
 	const auto newId = FullMsgId(history->channelId(), clientMsgId());
 	const auto channelPost = peer->isChannel() && !peer->isMegagroup();
-	const auto silentPost = channelPost && peer->notifySilentPosts();
 
 	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;
 	}
 	if (channelPost) {
 		flags |= MTPDmessage::Flag::f_views;
@@ -2588,14 +2673,11 @@ void ApiWrap::sendSharedContact(
 	} else {
 		flags |= MTPDmessage::Flag::f_from_id;
 	}
-	if (silentPost) {
-		sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
-	}
 	const auto messageFromId = channelPost ? 0 : Auth().userId();
 	const auto messagePostAuthor = channelPost
 		? (Auth().user()->firstName + ' ' + Auth().user()->lastName)
 		: QString();
-	history->addNewMessage(
+	const auto item = history->addNewMessage(
 		MTP_message(
 			MTP_flags(flags),
 			MTP_int(newId.msg),
@@ -2619,35 +2701,310 @@ void ApiWrap::sendSharedContact(
 			MTPlong()),
 		NewMessageUnread);
 
+	const auto media = MTP_inputMediaContact(
+		MTP_string(phone),
+		MTP_string(firstName),
+		MTP_string(lastName));
+	sendMedia(item, media, peer->notifySilentPosts());
+}
+
+void ApiWrap::sendVoiceMessage(
+		QByteArray result,
+		VoiceWaveform waveform,
+		int duration,
+		const SendOptions &options) {
+	const auto caption = QString();
+	const auto to = FileLoadTaskOptions(options);
+	_fileLoader->addTask(std::make_unique<FileLoadTask>(
+		result,
+		duration,
+		waveform,
+		to,
+		caption));
+}
+
+void ApiWrap::sendFiles(
+		Storage::PreparedList &&list,
+		const QByteArray &content,
+		const QImage &image,
+		SendMediaType type,
+		QString caption,
+		std::shared_ptr<SendingAlbum> album,
+		const SendOptions &options) {
+	if (list.files.size() > 1 && !caption.isEmpty()) {
+		auto message = MainWidget::MessageToSend(options.history);
+		message.textWithTags = { caption, TextWithTags::Tags() };
+		message.replyTo = options.replyTo;
+		message.clearDraft = false;
+		App::main()->sendMessage(message);
+		caption = QString();
+	}
+
+	const auto to = FileLoadTaskOptions(options);
+	auto tasks = std::vector<std::unique_ptr<Task>>();
+	tasks.reserve(list.files.size());
+	for (auto &file : list.files) {
+		if (album) {
+			switch (file.type) {
+			case Storage::PreparedFile::AlbumType::Photo:
+				type = SendMediaType::Photo;
+				break;
+			case Storage::PreparedFile::AlbumType::Video:
+				type = SendMediaType::File;
+				break;
+			default: Unexpected("AlbumType in uploadFilesAfterConfirmation");
+			}
+		}
+		if (file.path.isEmpty() && (!image.isNull() || !content.isNull())) {
+			tasks.push_back(std::make_unique<FileLoadTask>(
+				content,
+				image,
+				type,
+				to,
+				caption,
+				album));
+		} else {
+			tasks.push_back(std::make_unique<FileLoadTask>(
+				file.path,
+				std::move(file.information),
+				type,
+				to,
+				caption,
+				album));
+		}
+	}
+	if (album) {
+		_sendingAlbums.emplace(album->groupId, album);
+		album->items.reserve(tasks.size());
+		for (const auto &task : tasks) {
+			album->items.push_back(SendingAlbum::Item(task->id()));
+		}
+	}
+	_fileLoader->addTasks(std::move(tasks));
+}
+
+void ApiWrap::sendFile(
+		const QByteArray &fileContent,
+		SendMediaType type,
+		const SendOptions &options) {
+	auto to = FileLoadTaskOptions(options);
+	auto caption = QString();
+	_fileLoader->addTask(std::make_unique<FileLoadTask>(
+		fileContent,
+		QImage(),
+		type,
+		to,
+		caption));
+}
+
+void ApiWrap::sendUploadedPhoto(
+		FullMsgId localId,
+		const MTPInputFile &file,
+		bool silent) {
+	if (const auto item = App::histItemById(localId)) {
+		const auto caption = item->getMedia()
+			? item->getMedia()->getCaption()
+			: TextWithEntities();
+		const auto media = MTP_inputMediaUploadedPhoto(
+			MTP_flags(0),
+			file,
+			MTP_string(caption.text),
+			MTPVector<MTPInputDocument>(),
+			MTP_int(0));
+		if (const auto groupId = item->groupId()) {
+			uploadAlbumMedia(item, groupId, media, silent);
+		} else {
+			sendMedia(item, media, silent);
+		}
+	}
+}
+
+void ApiWrap::sendUploadedDocument(
+		FullMsgId localId,
+		const MTPInputFile &file,
+		const base::optional<MTPInputFile> &thumb,
+		bool silent) {
+	if (const auto item = App::histItemById(localId)) {
+		auto media = item->getMedia();
+		if (auto document = media ? media->getDocument() : nullptr) {
+			const auto caption = media->getCaption();
+			const auto groupId = item->groupId();
+			const auto flags = MTPDinputMediaUploadedDocument::Flags(0)
+				| (thumb
+					? MTPDinputMediaUploadedDocument::Flag::f_thumb
+					: MTPDinputMediaUploadedDocument::Flag(0))
+				| (groupId
+					? MTPDinputMediaUploadedDocument::Flag::f_nosound_video
+					: MTPDinputMediaUploadedDocument::Flag(0));
+			const auto media = MTP_inputMediaUploadedDocument(
+				MTP_flags(flags),
+				file,
+				thumb ? *thumb : MTPInputFile(),
+				MTP_string(document->mimeString()),
+				ComposeSendingDocumentAttributes(document),
+				MTP_string(caption.text),
+				MTPVector<MTPInputDocument>(),
+				MTP_int(0));
+			if (groupId) {
+				uploadAlbumMedia(item, groupId, media, silent);
+			} else {
+				sendMedia(item, media, silent);
+			}
+		}
+	}
+}
+
+void ApiWrap::uploadAlbumMedia(
+		not_null<HistoryItem*> item,
+		const MessageGroupId &groupId,
+		const MTPInputMedia &media,
+		bool silent) {
+	const auto localId = item->fullId();
+	const auto failed = [this] {
+
+	};
+	request(MTPmessages_UploadMedia(
+		item->history()->peer->input,
+		media
+	)).done([=](const MTPMessageMedia &result) {
+		const auto item = App::histItemById(localId);
+		if (!item) {
+			failed();
+		}
+
+		switch (result.type()) {
+		case mtpc_messageMediaPhoto: {
+			const auto &data = result.c_messageMediaPhoto();
+			if (data.vphoto.type() != mtpc_photo) {
+				failed();
+				return;
+			}
+			const auto &photo = data.vphoto.c_photo();
+			const auto flags = MTPDinputMediaPhoto::Flags(0)
+				| (data.has_ttl_seconds()
+					? MTPDinputMediaPhoto::Flag::f_ttl_seconds
+					: MTPDinputMediaPhoto::Flag(0));
+			const auto media = MTP_inputMediaPhoto(
+				MTP_flags(flags),
+				MTP_inputPhoto(photo.vid, photo.vaccess_hash),
+				data.has_caption() ? data.vcaption : MTP_string(QString()),
+				data.has_ttl_seconds() ? data.vttl_seconds : MTPint());
+			trySendAlbum(item, groupId, media, silent);
+		} break;
+
+		case mtpc_messageMediaDocument: {
+			const auto &data = result.c_messageMediaDocument();
+			if (data.vdocument.type() != mtpc_document) {
+				failed();
+				return;
+			}
+			const auto &document = data.vdocument.c_document();
+			const auto flags = MTPDinputMediaDocument::Flags(0)
+				| (data.has_ttl_seconds()
+					? MTPDinputMediaDocument::Flag::f_ttl_seconds
+					: MTPDinputMediaDocument::Flag(0));
+			const auto media = MTP_inputMediaDocument(
+				MTP_flags(flags),
+				MTP_inputDocument(document.vid, document.vaccess_hash),
+				data.has_caption() ? data.vcaption : MTP_string(QString()),
+				data.has_ttl_seconds() ? data.vttl_seconds : MTPint());
+			trySendAlbum(item, groupId, media, silent);
+		} break;
+		}
+	}).fail([=](const RPCError &error) {
+		failed();
+	}).send();
+}
+
+void ApiWrap::trySendAlbum(
+		not_null<HistoryItem*> item,
+		const MessageGroupId &groupId,
+		const MTPInputMedia &media,
+		bool silent) {
+	const auto localId = item->fullId();
+	const auto randomId = rand_value<uint64>();
+	App::historyRegRandom(randomId, localId);
+
+	const auto medias = completeAlbum(localId, groupId, media, randomId);
+	if (medias.empty()) {
+		return;
+	}
+
+	const auto history = item->history();
+	const auto replyTo = item->replyToId();
+	const auto flags = MTPmessages_SendMultiMedia::Flags(0)
+		| (replyTo
+			? MTPmessages_SendMultiMedia::Flag::f_reply_to_msg_id
+			: MTPmessages_SendMultiMedia::Flag(0))
+		| (IsSilentPost(item, silent)
+			? MTPmessages_SendMultiMedia::Flag::f_silent
+			: MTPmessages_SendMultiMedia::Flag(0));
+	history->sendRequestId = request(MTPmessages_SendMultiMedia(
+		MTP_flags(flags),
+		history->peer->input,
+		MTP_int(replyTo),
+		MTP_vector<MTPInputSingleMedia>(medias)
+	)).done([=](const MTPUpdates &result) { applyUpdates(result);
+	}).fail([=](const RPCError &error) { sendMessageFail(error);
+	}).afterRequest(history->sendRequestId
+	).send();
+}
+
+void ApiWrap::sendMedia(
+		not_null<HistoryItem*> item,
+		const MTPInputMedia &media,
+		bool silent) {
+	const auto randomId = rand_value<uint64>();
+	App::historyRegRandom(randomId, item->fullId());
+
+	const auto history = item->history();
+	const auto replyTo = item->replyToId();
+	const auto flags = MTPmessages_SendMedia::Flags(0)
+		| (replyTo
+			? MTPmessages_SendMedia::Flag::f_reply_to_msg_id
+			: MTPmessages_SendMedia::Flag(0))
+		| (IsSilentPost(item, silent)
+			? MTPmessages_SendMedia::Flag::f_silent
+			: MTPmessages_SendMedia::Flag(0));
 	history->sendRequestId = request(MTPmessages_SendMedia(
-		MTP_flags(sendFlags),
-		peer->input,
-		MTP_int(options.replyTo),
-		MTP_inputMediaContact(
-			MTP_string(phone),
-			MTP_string(firstName),
-			MTP_string(lastName)),
+		MTP_flags(flags),
+		history->peer->input,
+		MTP_int(replyTo),
+		media,
 		MTP_long(randomId),
 		MTPnullMarkup
-	)).done([=](const MTPUpdates &result) {
-		applyUpdates(result);
-	}).fail([](const RPCError &error) {
-		if (error.type() == qstr("PEER_FLOOD")) {
-			Ui::show(Box<InformBox>(
-				PeerFloodErrorText(PeerFloodType::Send)));
-		} else if (error.type() == qstr("USER_BANNED_IN_CHANNEL")) {
-			const auto link = textcmdLink(
-				Messenger::Instance().createInternalLinkFull(qsl("spambot")),
-				lang(lng_cant_more_info));
-			Ui::show(Box<InformBox>(lng_error_public_groups_denied(
-				lt_more_info,
-				link)));
-		}
-	}).afterRequest(
-		history->sendRequestId
+	)).done([=](const MTPUpdates &result) { applyUpdates(result);
+	}).fail([=](const RPCError &error) { sendMessageFail(error);
+	}).afterRequest(history->sendRequestId
 	).send();
+}
 
-	App::historyRegRandom(randomId, newId);
+QVector<MTPInputSingleMedia> ApiWrap::completeAlbum(
+		FullMsgId localId,
+		const MessageGroupId &groupId,
+		const MTPInputMedia &media,
+		uint64 randomId) {
+	const auto albumIt = _sendingAlbums.find(groupId.raw());
+	Assert(albumIt != _sendingAlbums.end());
+	const auto &album = albumIt->second;
+
+	const auto proj = [](const SendingAlbum::Item &item) {
+		return item.msgId;
+	};
+	const auto itemIt = ranges::find(album->items, localId, proj);
+	Assert(itemIt != album->items.end());
+	Assert(!itemIt->media);
+	itemIt->media = MTP_inputSingleMedia(media, MTP_long(randomId));
+
+	auto result = QVector<MTPInputSingleMedia>();
+	result.reserve(album->items.size());
+	for (const auto &item : album->items) {
+		if (!item.media) {
+			return {};
+		}
+		result.push_back(*item.media);
+	}
+	return result;
 }
 
 void ApiWrap::readServerHistory(not_null<History*> history) {
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 6eec78b76..9b035cea7 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -28,14 +28,18 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "base/flat_set.h"
 #include "chat_helpers/stickers.h"
 
+class TaskQueue;
 class AuthSession;
+enum class SparseIdsLoadDirection;
+struct MessageGroupId;
+struct SendingAlbum;
+enum class SendMediaType;
 
 namespace Storage {
 enum class SharedMediaType : char;
+struct PreparedList;
 } // namespace Storage
 
-enum class SparseIdsLoadDirection;
-
 namespace Api {
 
 inline const MTPVector<MTPChat> *getChatsFromMessagesChats(const MTPmessages_Chats &chats) {
@@ -186,6 +190,34 @@ public:
 	void readServerHistory(not_null<History*> history);
 	void readServerHistoryForce(not_null<History*> history);
 
+	void sendVoiceMessage(
+		QByteArray result,
+		VoiceWaveform waveform,
+		int duration,
+		const SendOptions &options);
+	void sendFiles(
+		Storage::PreparedList &&list,
+		const QByteArray &content,
+		const QImage &image,
+		SendMediaType type,
+		QString caption,
+		std::shared_ptr<SendingAlbum> album,
+		const SendOptions &options);
+	void sendFile(
+		const QByteArray &fileContent,
+		SendMediaType type,
+		const SendOptions &options);
+
+	void sendUploadedPhoto(
+		FullMsgId localId,
+		const MTPInputFile &file,
+		bool silent);
+	void sendUploadedDocument(
+		FullMsgId localId,
+		const MTPInputFile &file,
+		const base::optional<MTPInputFile> &thumb,
+		bool silent);
+
 	~ApiWrap();
 
 private:
@@ -283,6 +315,26 @@ private:
 	void applyAffectedMessages(
 		not_null<PeerData*> peer,
 		const MTPmessages_AffectedMessages &result);
+	void sendMessageFail(const RPCError &error);
+	void uploadAlbumMedia(
+		not_null<HistoryItem*> item,
+		const MessageGroupId &groupId,
+		const MTPInputMedia &media,
+		bool silent);
+	void trySendAlbum(
+		not_null<HistoryItem*> item,
+		const MessageGroupId &groupId,
+		const MTPInputMedia &media,
+		bool silent);
+	QVector<MTPInputSingleMedia> completeAlbum(
+		FullMsgId localId,
+		const MessageGroupId &groupId,
+		const MTPInputMedia &media,
+		uint64 randomId);
+	void sendMedia(
+		not_null<HistoryItem*> item,
+		const MTPInputMedia &media,
+		bool silent);
 
 	not_null<AuthSession*> _session;
 	mtpRequestId _changelogSubscription = 0;
@@ -375,6 +427,8 @@ private:
 	};
 	base::flat_map<not_null<PeerData*>, ReadRequest> _readRequests;
 	base::flat_map<not_null<PeerData*>, MsgId> _readRequestsPending;
+	std::unique_ptr<TaskQueue> _fileLoader;
+	base::flat_map<uint64, std::shared_ptr<SendingAlbum>> _sendingAlbums;
 
 	base::Observable<PeerData*> _fullPeerUpdated;
 
diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp
index 1fa0d2d15..250fc5f5b 100644
--- a/Telegram/SourceFiles/app.cpp
+++ b/Telegram/SourceFiles/app.cpp
@@ -1394,7 +1394,7 @@ namespace {
 					::photosData.erase(i);
 				}
 				convert->id = photo;
-				convert->uploadingData.reset();
+				convert->uploadingData = nullptr;
 			}
 			if (date) {
 				convert->access = access;
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index 80ff306c4..3e8058791 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -716,3 +716,5 @@ groupStickersField: InputField(contactsSearchField) {
 	heightMin: 32px;
 }
 groupStickersSubTitleHeight: 36px;
+
+sendMediaPreviewSize: 308px;
diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp
index ad2b75e87..b533224af 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.cpp
+++ b/Telegram/SourceFiles/boxes/send_files_box.cpp
@@ -22,6 +22,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 
 #include "lang/lang_keys.h"
 #include "storage/localstorage.h"
+#include "storage/storage_media_prepare.h"
 #include "mainwidget.h"
 #include "history/history_media_types.h"
 #include "core/file_utilities.h"
@@ -29,6 +30,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/input_fields.h"
 #include "ui/empty_userpic.h"
+#include "ui/grouped_layout.h"
 #include "styles/style_history.h"
 #include "styles/style_boxes.h"
 #include "media/media_clip_reader.h"
@@ -38,36 +40,38 @@ namespace {
 
 constexpr auto kMinPreviewWidth = 20;
 
-bool ValidatePhotoDimensions(int width, int height) {
-	return (width > 0) && (height > 0) && (width < 20 * height) && (height < 20 * width);
-}
-
 } // namespace
 
 SendFilesBox::SendFilesBox(QWidget*, QImage image, CompressConfirm compressed)
 : _image(image)
 , _compressConfirm(compressed)
 , _caption(this, st::confirmCaptionArea, langFactory(lng_photo_caption)) {
-	_files.push_back(QString());
+	_list.files.push_back({ QString() });
 	prepareSingleFileLayout();
 }
 
-SendFilesBox::SendFilesBox(QWidget*, const QStringList &files, CompressConfirm compressed)
-: _files(files)
+SendFilesBox::SendFilesBox(QWidget*, Storage::PreparedList &&list, CompressConfirm compressed)
+: _list(std::move(list))
 , _compressConfirm(compressed)
-, _caption(this, st::confirmCaptionArea, langFactory(_files.size() > 1 ? lng_photos_comment : lng_photo_caption)) {
-	if (_files.size() == 1) {
+, _caption(
+	this,
+	st::confirmCaptionArea,
+	langFactory(_list.files.size() > 1
+		? lng_photos_comment
+		: lng_photo_caption)) {
+	if (_list.files.size() == 1) {
 		prepareSingleFileLayout();
 	}
 }
 
 void SendFilesBox::prepareSingleFileLayout() {
-	Expects(_files.size() == 1);
-	if (!_files.front().isEmpty()) {
+	Expects(_list.files.size() == 1);
+	if (!_list.files.front().path.isEmpty()) {
 		tryToReadSingleFile();
 	}
 
-	if (_image.isNull() || !ValidatePhotoDimensions(_image.width(), _image.height()) || _animated) {
+	if (!Storage::ValidateThumbDimensions(_image.width(), _image.height())
+		|| _animated) {
 		_compressConfirm = CompressConfirm::None;
 	}
 
@@ -86,7 +90,7 @@ void SendFilesBox::prepareSingleFileLayout() {
 			auto maxW = 0;
 			auto maxH = 0;
 			if (_animated) {
-				auto limitW = st::boxWideWidth - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
+				auto limitW = st::sendMediaPreviewSize;
 				auto limitH = st::confirmMaxHeight;
 				maxW = qMax(image.width(), 1);
 				maxH = qMax(image.height(), 1);
@@ -108,7 +112,7 @@ void SendFilesBox::prepareSingleFileLayout() {
 			if (!originalWidth || !originalHeight) {
 				originalWidth = originalHeight = 1;
 			}
-			_previewWidth = st::boxWideWidth - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
+			_previewWidth = st::sendMediaPreviewSize;
 			if (image.width() < _previewWidth) {
 				_previewWidth = qMax(image.width(), kMinPreviewWidth);
 			}
@@ -135,20 +139,26 @@ void SendFilesBox::prepareSingleFileLayout() {
 }
 
 void SendFilesBox::prepareGifPreview() {
+	using namespace Media::Clip;
 	auto createGifPreview = [this] {
-		if (!_information) {
+		const auto &information = _list.files.front().information;
+		if (!information) {
 			return false;
 		}
-		if (auto video = base::get_if<FileLoadTask::Video>(&_information->media)) {
+		if (const auto video = base::get_if<FileMediaInformation::Video>(
+				&information->media)) {
 			return video->isGifv;
 		}
 		// Plain old .gif animation.
 		return _animated;
 	};
 	if (createGifPreview()) {
-		_gifPreview = Media::Clip::MakeReader(_files.front(), [this](Media::Clip::Notification notification) {
+		const auto callback = [this](Notification notification) {
 			clipCallback(notification);
-		});
+		};
+		_gifPreview = Media::Clip::MakeReader(
+			_list.files.front().path,
+			callback);
 		if (_gifPreview) _gifPreview->setAutoplay();
 	}
 }
@@ -178,7 +188,8 @@ void SendFilesBox::clipCallback(Media::Clip::Notification notification) {
 }
 
 void SendFilesBox::prepareDocumentLayout() {
-	auto filepath = _files.front();
+	const auto &file = _list.files.front();
+	const auto filepath = file.path;
 	if (filepath.isEmpty()) {
 		auto filename = filedialogDefaultName(qsl("image"), qsl(".png"), QString(), true);
 		_nameText.setText(st::semiboldTextStyle, filename, _textNameOptions);
@@ -192,15 +203,16 @@ void SendFilesBox::prepareDocumentLayout() {
 
 		auto songTitle = QString();
 		auto songPerformer = QString();
-		if (_information) {
-			if (auto song = base::get_if<FileLoadTask::Song>(&_information->media)) {
+		if (file.information) {
+			if (const auto song = base::get_if<FileMediaInformation::Song>(
+					&file.information->media)) {
 				songTitle = song->title;
 				songPerformer = song->performer;
 				_fileIsAudio = true;
 			}
 		}
 
-		auto nameString = DocumentData::ComposeNameString(
+		const auto nameString = DocumentData::ComposeNameString(
 			filename,
 			songTitle,
 			songPerformer);
@@ -216,13 +228,21 @@ void SendFilesBox::prepareDocumentLayout() {
 }
 
 void SendFilesBox::tryToReadSingleFile() {
-	auto filepath = _files.front();
+	auto &file = _list.files.front();
+	auto filepath = file.path;
 	auto filemime = mimeTypeForFile(QFileInfo(filepath)).name();
-	_information = FileLoadTask::ReadMediaInformation(_files.front(), QByteArray(), filemime);
-	if (auto image = base::get_if<FileLoadTask::Image>(&_information->media)) {
+	if (!file.information) {
+		file.information = FileLoadTask::ReadMediaInformation(
+			filepath,
+			QByteArray(),
+			filemime);
+	}
+	if (const auto image = base::get_if<FileMediaInformation::Image>(
+			&file.information->media)) {
 		_image = image->data;
 		_animated = image->animated;
-	} else if (auto video = base::get_if<FileLoadTask::Video>(&_information->media)) {
+	} else if (const auto video = base::get_if<FileMediaInformation::Video>(
+			&file.information->media)) {
 		_image = video->thumbnail;
 		_animated = true;
 	}
@@ -244,25 +264,34 @@ SendFilesBox::SendFilesBox(QWidget*, const QString &phone, const QString &firstn
 void SendFilesBox::prepare() {
 	Expects(controller() != nullptr);
 
-	if (_files.size() > 1) {
+	if (_list.files.size() > 1) {
 		updateTitleText();
 	}
 
-	_send = addButton(langFactory(lng_send_button), [this] { onSend(); });
+	_send = addButton(langFactory(lng_send_button), [this] { send(); });
 	addButton(langFactory(lng_cancel), [this] { closeBox(); });
 
 	if (_compressConfirm != CompressConfirm::None) {
 		auto compressed = (_compressConfirm == CompressConfirm::Auto) ? cCompressPastedImage() : (_compressConfirm == CompressConfirm::Yes);
-		auto text = lng_send_images_compress(lt_count, _files.size());
+		auto text = lng_send_images_compress(lt_count, _list.files.size());
 		_compressed.create(this, text, compressed, st::defaultBoxCheckbox);
-		subscribe(_compressed->checkedChanged, [this](bool checked) { onCompressedChange(); });
+		subscribe(_compressed->checkedChanged, [this](bool checked) {
+			compressedChange();
+		});
 	}
 	if (_caption) {
 		_caption->setMaxLength(MaxPhotoCaption);
 		_caption->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
-		connect(_caption, SIGNAL(resized()), this, SLOT(onCaptionResized()));
-		connect(_caption, SIGNAL(submitted(bool)), this, SLOT(onSend(bool)));
-		connect(_caption, SIGNAL(cancelled()), this, SLOT(onClose()));
+		connect(_caption, &Ui::InputArea::resized, this, [this] {
+			captionResized();
+		});
+		connect(_caption, &Ui::InputArea::submitted, this, [this](
+				bool ctrlShiftEnter) {
+			send(ctrlShiftEnter);
+		});
+		connect(_caption, &Ui::InputArea::cancelled, this, [this] {
+			closeBox();
+		});
 	}
 	subscribe(boxClosing, [this] {
 		if (!_confirmed && _cancelledCallback) {
@@ -278,26 +307,32 @@ base::lambda<QString()> SendFilesBox::getSendButtonText() const {
 	if (!_contactPhone.isEmpty()) {
 		return langFactory(lng_send_button);
 	} else if (_compressed && _compressed->checked()) {
-		return [count = _files.size()] { return lng_send_photos(lt_count, count); };
+		return [count = _list.files.size()] {
+			return lng_send_photos(lt_count, count);
+		};
 	}
-	return [count = _files.size()] { return lng_send_files(lt_count, count); };
+	return [count = _list.files.size()] {
+		return lng_send_files(lt_count, count);
+	};
 }
 
-void SendFilesBox::onCompressedChange() {
+void SendFilesBox::compressedChange() {
 	setInnerFocus();
 	_send->setText(getSendButtonText());
 	updateButtonsGeometry();
 	updateControlsGeometry();
 }
 
-void SendFilesBox::onCaptionResized() {
+void SendFilesBox::captionResized() {
 	updateBoxSize();
 	updateControlsGeometry();
 	update();
 }
 
 void SendFilesBox::updateTitleText() {
-	_titleText = (_compressConfirm == CompressConfirm::None) ? lng_send_files_selected(lt_count, _files.size()) : lng_send_images_selected(lt_count, _files.size());
+	_titleText = (_compressConfirm == CompressConfirm::None)
+		? lng_send_files_selected(lt_count, _list.files.size())
+		: lng_send_images_selected(lt_count, _list.files.size());
 	update();
 }
 
@@ -307,7 +342,7 @@ void SendFilesBox::updateBoxSize() {
 		newHeight += st::boxPhotoPadding.top() + _previewHeight;
 	} else if (!_fileThumb.isNull()) {
 		newHeight += st::boxPhotoPadding.top() + st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom();
-	} else if (_files.size() > 1) {
+	} else if (_list.files.size() > 1) {
 		newHeight += 0;
 	} else {
 		newHeight += st::boxPhotoPadding.top() + st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom();
@@ -323,7 +358,11 @@ void SendFilesBox::updateBoxSize() {
 
 void SendFilesBox::keyPressEvent(QKeyEvent *e) {
 	if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
-		onSend((e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier)) && e->modifiers().testFlag(Qt::ShiftModifier));
+		const auto modifiers = e->modifiers();
+		const auto ctrl = modifiers.testFlag(Qt::ControlModifier)
+			|| modifiers.testFlag(Qt::MetaModifier);
+		const auto shift = modifiers.testFlag(Qt::ShiftModifier);
+		send(ctrl && shift);
 	} else {
 		BoxContent::keyPressEvent(e);
 	}
@@ -368,7 +407,7 @@ void SendFilesBox::paintEvent(QPaintEvent *e) {
 			auto icon = &st::historyFileInPlay;
 			icon->paintInCenter(p, inner);
 		}
-	} else if (_files.size() < 2) {
+	} else if (_list.files.size() < 2) {
 		auto w = width() - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
 		auto h = _fileThumb.isNull() ? (st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom()) : (st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom());
 		auto nameleft = 0, nametop = 0, nameright = 0, statustop = 0, linktop = 0;
@@ -428,7 +467,7 @@ void SendFilesBox::resizeEvent(QResizeEvent *e) {
 void SendFilesBox::updateControlsGeometry() {
 	auto bottom = height();
 	if (_caption) {
-		_caption->resize(st::boxWideWidth - st::boxPhotoPadding.left() - st::boxPhotoPadding.right(), _caption->height());
+		_caption->resize(st::sendMediaPreviewSize, _caption->height());
 		_caption->moveToLeft(st::boxPhotoPadding.left(), bottom - _caption->height());
 		bottom -= st::boxPhotoCaptionSkip + _caption->height();
 	}
@@ -446,7 +485,7 @@ void SendFilesBox::setInnerFocus() {
 	}
 }
 
-void SendFilesBox::onSend(bool ctrlShiftEnter) {
+void SendFilesBox::send(bool ctrlShiftEnter) {
 	if (_compressed && _compressConfirm == CompressConfirm::Auto && _compressed->checked() != cCompressPastedImage()) {
 		cSetCompressPastedImage(_compressed->checked());
 		Local::writeUserSettings();
@@ -455,13 +494,201 @@ void SendFilesBox::onSend(bool ctrlShiftEnter) {
 	if (_confirmedCallback) {
 		auto compressed = _compressed ? _compressed->checked() : false;
 		auto caption = _caption ? TextUtilities::PrepareForSending(_caption->getLastText(), TextUtilities::PrepareTextOption::CheckLinks) : QString();
-		_confirmedCallback(_files, _animated ? QImage() : _image, std::move(_information), compressed, caption, ctrlShiftEnter);
+		_confirmedCallback(
+			std::move(_list),
+			_animated ? QImage() : _image,
+			compressed,
+			caption,
+			ctrlShiftEnter);
 	}
 	closeBox();
 }
 
 SendFilesBox::~SendFilesBox() = default;
 
+struct SendAlbumBox::Thumb {
+	Ui::GroupMediaLayout layout;
+	QPixmap image;
+};
+
+SendAlbumBox::SendAlbumBox(QWidget*, Storage::PreparedList &&list)
+: _list(std::move(list))
+, _caption(
+	this,
+	st::confirmCaptionArea,
+	langFactory(_list.files.size() > 1
+		? lng_photos_comment
+		: lng_photo_caption)) {
+}
+
+void SendAlbumBox::prepare() {
+	Expects(controller() != nullptr);
+
+	prepareThumbs();
+
+	addButton(langFactory(lng_send_button), [this] { send(); });
+	addButton(langFactory(lng_cancel), [this] { closeBox(); });
+
+	if (_caption) {
+		_caption->setMaxLength(MaxPhotoCaption);
+		_caption->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
+		connect(_caption, &Ui::InputArea::resized, this, [this] {
+			captionResized();
+		});
+		connect(_caption, &Ui::InputArea::submitted, this, [this](
+				bool ctrlShiftEnter) {
+			send(ctrlShiftEnter);
+		});
+		connect(_caption, &Ui::InputArea::cancelled, this, [this] {
+			closeBox();
+		});
+	}
+	subscribe(boxClosing, [this] {
+		if (!_confirmed && _cancelledCallback) {
+			_cancelledCallback();
+		}
+	});
+
+	updateButtonsGeometry();
+	updateBoxSize();
+}
+
+void SendAlbumBox::prepareThumbs() {
+	auto sizes = ranges::view::all(
+		_list.files
+	) | ranges::view::transform([](const Storage::PreparedFile &file) {
+		return file.preview.size() / cIntRetinaFactor();
+	}) | ranges::to_vector;
+
+	const auto count = int(sizes.size());
+	const auto layout = Ui::LayoutMediaGroup(
+		sizes,
+		st::sendMediaPreviewSize,
+		st::historyGroupWidthMin / 2,
+		st::historyGroupSkip / 2);
+	Assert(layout.size() == count);
+
+	_thumbs.reserve(count);
+	for (auto i = 0; i != count; ++i) {
+		_thumbs.push_back(prepareThumb(_list.files[i].preview, layout[i]));
+		const auto &geometry = layout[i].geometry;
+		accumulate_max(_thumbsHeight, geometry.y() + geometry.height());
+	}
+}
+
+SendAlbumBox::Thumb SendAlbumBox::prepareThumb(
+		const QImage &preview,
+		const Ui::GroupMediaLayout &layout) const {
+	auto result = Thumb();
+	result.layout = layout;
+
+	const auto width = layout.geometry.width();
+	const auto height = layout.geometry.height();
+	const auto corners = Ui::GetCornersFromSides(layout.sides);
+	using Option = Images::Option;
+	const auto options = Option::Smooth
+		| Option::RoundedLarge
+		| ((corners & RectPart::TopLeft) ? Option::RoundedTopLeft : Option::None)
+		| ((corners & RectPart::TopRight) ? Option::RoundedTopRight : Option::None)
+		| ((corners & RectPart::BottomLeft) ? Option::RoundedBottomLeft : Option::None)
+		| ((corners & RectPart::BottomRight) ? Option::RoundedBottomRight : Option::None);
+	const auto pixSize = Ui::GetImageScaleSizeForGeometry(
+		{ preview.width(), preview.height() },
+		{ width, height });
+	const auto pixWidth = pixSize.width() * cIntRetinaFactor();
+	const auto pixHeight = pixSize.height() * cIntRetinaFactor();
+
+	result.image = App::pixmapFromImageInPlace(Images::prepare(
+		preview,
+		pixWidth,
+		pixHeight,
+		options,
+		width,
+		height));
+	return result;
+}
+
+void SendAlbumBox::captionResized() {
+	updateBoxSize();
+	updateControlsGeometry();
+	update();
+}
+
+void SendAlbumBox::updateBoxSize() {
+	auto newHeight = st::boxPhotoPadding.top() + _thumbsHeight;
+	if (_caption) {
+		newHeight += st::boxPhotoCaptionSkip + _caption->height();
+	}
+	setDimensions(st::boxWideWidth, newHeight);
+}
+
+void SendAlbumBox::keyPressEvent(QKeyEvent *e) {
+	if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
+		const auto modifiers = e->modifiers();
+		const auto ctrl = modifiers.testFlag(Qt::ControlModifier)
+			|| modifiers.testFlag(Qt::MetaModifier);
+		const auto shift = modifiers.testFlag(Qt::ShiftModifier);
+		send(ctrl && shift);
+	} else {
+		BoxContent::keyPressEvent(e);
+	}
+}
+
+void SendAlbumBox::paintEvent(QPaintEvent *e) {
+	BoxContent::paintEvent(e);
+
+	Painter p(this);
+
+	const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
+	const auto top = st::boxPhotoPadding.top();
+	for (const auto &thumb : _thumbs) {
+		p.drawPixmap(
+			left + thumb.layout.geometry.x(),
+			top + thumb.layout.geometry.y(),
+			thumb.image);
+	}
+}
+
+void SendAlbumBox::resizeEvent(QResizeEvent *e) {
+	BoxContent::resizeEvent(e);
+	updateControlsGeometry();
+}
+
+void SendAlbumBox::updateControlsGeometry() {
+	auto bottom = height();
+	if (_caption) {
+		_caption->resize(st::sendMediaPreviewSize, _caption->height());
+		_caption->moveToLeft(st::boxPhotoPadding.left(), bottom - _caption->height());
+		bottom -= st::boxPhotoCaptionSkip + _caption->height();
+	}
+}
+
+void SendAlbumBox::setInnerFocus() {
+	if (!_caption || _caption->isHidden()) {
+		setFocus();
+	} else {
+		_caption->setFocusFast();
+	}
+}
+
+void SendAlbumBox::send(bool ctrlShiftEnter) {
+	_confirmed = true;
+	if (_confirmedCallback) {
+		auto caption = _caption
+			? TextUtilities::PrepareForSending(
+				_caption->getLastText(),
+				TextUtilities::PrepareTextOption::CheckLinks)
+			: QString();
+		_confirmedCallback(
+			std::move(_list),
+			caption,
+			ctrlShiftEnter);
+	}
+	closeBox();
+}
+
+SendAlbumBox::~SendAlbumBox() = default;
+
 EditCaptionBox::EditCaptionBox(QWidget*, HistoryMedia *media, FullMsgId msgId) : _msgId(msgId) {
 	Expects(media->canEditCaption());
 
@@ -545,7 +772,7 @@ EditCaptionBox::EditCaptionBox(QWidget*, HistoryMedia *media, FullMsgId msgId) :
 	} else {
 		int32 maxW = 0, maxH = 0;
 		if (_animated) {
-			int32 limitW = st::boxWideWidth - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
+			int32 limitW = st::sendMediaPreviewSize;
 			int32 limitH = st::confirmMaxHeight;
 			maxW = qMax(dimensions.width(), 1);
 			maxH = qMax(dimensions.height(), 1);
@@ -571,7 +798,7 @@ EditCaptionBox::EditCaptionBox(QWidget*, HistoryMedia *media, FullMsgId msgId) :
 		if (!tw || !th) {
 			tw = th = 1;
 		}
-		_thumbw = st::boxWideWidth - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
+		_thumbw = st::sendMediaPreviewSize;
 		if (_thumb.width() < _thumbw) {
 			_thumbw = (_thumb.width() > 20) ? _thumb.width() : 20;
 		}
@@ -634,20 +861,24 @@ void EditCaptionBox::clipCallback(Media::Clip::Notification notification) {
 }
 
 void EditCaptionBox::prepare() {
-	addButton(langFactory(lng_settings_save), [this] { onSave(); });
+	addButton(langFactory(lng_settings_save), [this] { save(); });
 	addButton(langFactory(lng_cancel), [this] { closeBox(); });
 
 	updateBoxSize();
-	connect(_field, SIGNAL(submitted(bool)), this, SLOT(onSave(bool)));
-	connect(_field, SIGNAL(cancelled()), this, SLOT(onClose()));
-	connect(_field, SIGNAL(resized()), this, SLOT(onCaptionResized()));
+	connect(_field, &Ui::InputArea::submitted, this, [this] { save(); });
+	connect(_field, &Ui::InputArea::cancelled, this, [this] {
+		closeBox();
+	});
+	connect(_field, &Ui::InputArea::resized, this, [this] {
+		captionResized();
+	});
 
 	auto cursor = _field->textCursor();
 	cursor.movePosition(QTextCursor::End);
 	_field->setTextCursor(cursor);
 }
 
-void EditCaptionBox::onCaptionResized() {
+void EditCaptionBox::captionResized() {
 	updateBoxSize();
 	resizeEvent(0);
 	update();
@@ -767,7 +998,7 @@ void EditCaptionBox::paintEvent(QPaintEvent *e) {
 
 void EditCaptionBox::resizeEvent(QResizeEvent *e) {
 	BoxContent::resizeEvent(e);
-	_field->resize(st::boxWideWidth - st::boxPhotoPadding.left() - st::boxPhotoPadding.right(), _field->height());
+	_field->resize(st::sendMediaPreviewSize, _field->height());
 	_field->moveToLeft(st::boxPhotoPadding.left(), height() - st::normalFont->height - errorTopSkip() - _field->height());
 }
 
@@ -775,7 +1006,7 @@ void EditCaptionBox::setInnerFocus() {
 	_field->setFocusFast();
 }
 
-void EditCaptionBox::onSave(bool ctrlShiftEnter) {
+void EditCaptionBox::save() {
 	if (_saveRequestId) return;
 
 	auto item = App::histItemById(_msgId);
diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h
index f17ffefc4..be762c72d 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.h
+++ b/Telegram/SourceFiles/boxes/send_files_box.h
@@ -22,23 +22,23 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 
 #include "boxes/abstract_box.h"
 #include "storage/localimageloader.h"
+#include "storage/storage_media_prepare.h"
 
 namespace Ui {
 class Checkbox;
 class RoundButton;
 class InputArea;
 class EmptyUserpic;
+struct GroupMediaLayout;
 } // namespace Ui
 
 class SendFilesBox : public BoxContent {
-	Q_OBJECT
-
 public:
 	SendFilesBox(QWidget*, QImage image, CompressConfirm compressed);
-	SendFilesBox(QWidget*, const QStringList &files, CompressConfirm compressed);
+	SendFilesBox(QWidget*, Storage::PreparedList &&list, CompressConfirm compressed);
 	SendFilesBox(QWidget*, const QString &phone, const QString &firstname, const QString &lastname);
 
-	void setConfirmedCallback(base::lambda<void(const QStringList &files, const QImage &image, std::unique_ptr<FileLoadTask::MediaInformation> information, bool compressed, const QString &caption, bool ctrlShiftEnter)> callback) {
+	void setConfirmedCallback(base::lambda<void(Storage::PreparedList &&list, const QImage &image, bool compressed, const QString &caption, bool ctrlShiftEnter)> callback) {
 		_confirmedCallback = std::move(callback);
 	}
 	void setCancelledCallback(base::lambda<void()> callback) {
@@ -55,14 +55,6 @@ protected:
 	void paintEvent(QPaintEvent *e) override;
 	void resizeEvent(QResizeEvent *e) override;
 
-private slots:
-	void onCompressedChange();
-	void onSend(bool ctrlShiftEnter = false);
-	void onCaptionResized();
-	void onClose() {
-		closeBox();
-	}
-
 private:
 	void prepareSingleFileLayout();
 	void prepareDocumentLayout();
@@ -70,15 +62,18 @@ private:
 	void prepareGifPreview();
 	void clipCallback(Media::Clip::Notification notification);
 
+	void send(bool ctrlShiftEnter = false);
+	void captionResized();
+	void compressedChange();
+
 	void updateTitleText();
 	void updateBoxSize();
 	void updateControlsGeometry();
 	base::lambda<QString()> getSendButtonText() const;
 
 	QString _titleText;
-	QStringList _files;
+	Storage::PreparedList _list;
 	QImage _image;
-	std::unique_ptr<FileLoadTask::MediaInformation> _information;
 
 	CompressConfirm _compressConfirm = CompressConfirm::None;
 	bool _animated = false;
@@ -101,7 +96,7 @@ private:
 	QString _contactLastName;
 	std::unique_ptr<Ui::EmptyUserpic> _contactPhotoEmpty;
 
-	base::lambda<void(const QStringList &files, const QImage &image, std::unique_ptr<FileLoadTask::MediaInformation> information, bool compressed, const QString &caption, bool ctrlShiftEnter)> _confirmedCallback;
+	base::lambda<void(Storage::PreparedList &&list, const QImage &image, bool compressed, const QString &caption, bool ctrlShiftEnter)> _confirmedCallback;
 	base::lambda<void()> _cancelledCallback;
 	bool _confirmed = false;
 
@@ -112,19 +107,58 @@ private:
 
 };
 
-class EditCaptionBox : public BoxContent, public RPCSender {
-	Q_OBJECT
+class SendAlbumBox : public BoxContent {
+public:
+	SendAlbumBox(QWidget*, Storage::PreparedList &&list);
 
+	void setConfirmedCallback(base::lambda<void(Storage::PreparedList &&list, const QString &caption, bool ctrlShiftEnter)> callback) {
+		_confirmedCallback = std::move(callback);
+	}
+	void setCancelledCallback(base::lambda<void()> callback) {
+		_cancelledCallback = std::move(callback);
+	}
+
+	~SendAlbumBox();
+
+protected:
+	void prepare() override;
+	void setInnerFocus() override;
+
+	void keyPressEvent(QKeyEvent *e) override;
+	void paintEvent(QPaintEvent *e) override;
+	void resizeEvent(QResizeEvent *e) override;
+
+private:
+	struct Thumb;
+
+	void prepareThumbs();
+	Thumb prepareThumb(
+		const QImage &preview,
+		const Ui::GroupMediaLayout &layout) const;
+
+	void send(bool ctrlShiftEnter = false);
+	void captionResized();
+
+	void updateBoxSize();
+	void updateControlsGeometry();
+
+	Storage::PreparedList _list;
+
+	std::vector<Thumb> _thumbs;
+	int _thumbsHeight = 0;
+
+	base::lambda<void(Storage::PreparedList &&list, const QString &caption, bool ctrlShiftEnter)> _confirmedCallback;
+	base::lambda<void()> _cancelledCallback;
+	bool _confirmed = false;
+
+	object_ptr<Ui::InputArea> _caption = { nullptr };
+
+};
+
+class EditCaptionBox : public BoxContent, public RPCSender {
 public:
 	EditCaptionBox(QWidget*, HistoryMedia *media, FullMsgId msgId);
 
-public slots:
-	void onCaptionResized();
-	void onSave(bool ctrlShiftEnter = false);
-	void onClose() {
-		closeBox();
-	}
-
 protected:
 	void prepare() override;
 	void setInnerFocus() override;
@@ -137,6 +171,9 @@ private:
 	void prepareGifPreview(DocumentData *document);
 	void clipCallback(Media::Clip::Notification notification);
 
+	void save();
+	void captionResized();
+
 	void saveDone(const MTPUpdates &updates);
 	bool saveFail(const RPCError &error);
 
diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h
index e6eee71c6..f109cd9e1 100644
--- a/Telegram/SourceFiles/config.h
+++ b/Telegram/SourceFiles/config.h
@@ -285,8 +285,6 @@ enum {
 	DialogsFirstLoad = 20, // first dialogs part size requested
 	DialogsPerPage = 500, // next dialogs part size
 
-	FileLoaderQueueStopTimeout = 5000,
-
     UseBigFilesFrom = 10 * 1024 * 1024, // mtp big files methods used for files greater than 10mb
 
 	UploadPartSize = 32 * 1024, // 32kb for photo
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index e75542f54..c42c9af31 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -38,6 +38,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "window/window_controller.h"
 #include "window/window_slide_animation.h"
 #include "profile/profile_channel_controllers.h"
+#include "storage/storage_media_prepare.h"
 
 namespace {
 
@@ -745,18 +746,22 @@ bool DialogsWidget::peopleFailed(const RPCError &error, mtpRequestId req) {
 }
 
 void DialogsWidget::dragEnterEvent(QDragEnterEvent *e) {
+	using namespace Storage;
+
 	if (App::main()->selectingPeer()) return;
 
+	const auto data = e->mimeData();
 	_dragInScroll = false;
-	_dragForward = e->mimeData()->hasFormat(qsl("application/x-td-forward-selected"));
-	if (!_dragForward) _dragForward = e->mimeData()->hasFormat(qsl("application/x-td-forward-pressed-link"));
-	if (!_dragForward) _dragForward = e->mimeData()->hasFormat(qsl("application/x-td-forward-pressed"));
-	if (_dragForward && Adaptive::OneColumn()) _dragForward = false;
+	_dragForward = Adaptive::OneColumn()
+		? false
+		: (data->hasFormat(qsl("application/x-td-forward-selected"))
+			|| data->hasFormat(qsl("application/x-td-forward-pressed-link"))
+			|| data->hasFormat(qsl("application/x-td-forward-pressed")));
 	if (_dragForward) {
 		e->setDropAction(Qt::CopyAction);
 		e->accept();
 		updateDragInScroll(_scroll->geometry().contains(e->pos()));
-	} else if (App::main() && App::main()->getDragState(e->mimeData()) != DragState::None) {
+	} else if (ComputeMimeDataState(data) != MimeDataState::None) {
 		e->setDropAction(Qt::CopyAction);
 		e->accept();
 	}
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 0cbf3d5ba..4d95f3942 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -42,6 +42,9 @@ struct MessageGroupId {
 	explicit operator bool() const {
 		return value != None;
 	}
+	Underlying raw() const {
+		return static_cast<Underlying>(value);
+	}
 
 	friend inline Type value_ordering_helper(MessageGroupId value) {
 		return value.value;
diff --git a/Telegram/SourceFiles/history/history_media_grouped.cpp b/Telegram/SourceFiles/history/history_media_grouped.cpp
index 6c0bd973c..942de592c 100644
--- a/Telegram/SourceFiles/history/history_media_grouped.cpp
+++ b/Telegram/SourceFiles/history/history_media_grouped.cpp
@@ -28,26 +28,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "ui/grouped_layout.h"
 #include "styles/style_history.h"
 
-namespace {
-
-RectParts GetCornersFromSides(RectParts sides) {
-	const auto convert = [&](
-			RectPart side1,
-			RectPart side2,
-			RectPart corner) {
-		return ((sides & side1) && (sides & side2))
-			? corner
-			: RectPart::None;
-	};
-	return RectPart::None
-		| convert(RectPart::Top, RectPart::Left, RectPart::TopLeft)
-		| convert(RectPart::Top, RectPart::Right, RectPart::TopRight)
-		| convert(RectPart::Bottom, RectPart::Left, RectPart::BottomLeft)
-		| convert(RectPart::Bottom, RectPart::Right, RectPart::BottomRight);
-}
-
-} // namespace
-
 HistoryGroupedMedia::Element::Element(not_null<HistoryItem*> item)
 : item(item) {
 }
@@ -62,6 +42,12 @@ HistoryGroupedMedia::HistoryGroupedMedia(
 	Ensures(result);
 }
 
+std::unique_ptr<HistoryMedia> HistoryGroupedMedia::clone(
+		not_null<HistoryItem*> newParent,
+		not_null<HistoryItem*> realParent) const {
+	return main()->clone(newParent, realParent);
+}
+
 void HistoryGroupedMedia::initDimensions() {
 	if (_caption.hasSkipBlock()) {
 		_caption.setSkipBlock(
@@ -77,7 +63,7 @@ void HistoryGroupedMedia::initDimensions() {
 		sizes.push_back(media->sizeForGrouping());
 	}
 
-	const auto layout = Data::LayoutMediaGroup(
+	const auto layout = Ui::LayoutMediaGroup(
 		sizes,
 		st::historyGroupWidthMax,
 		st::historyGroupWidthMin,
@@ -171,7 +157,7 @@ void HistoryGroupedMedia::draw(
 			: IsGroupItemSelection(selection, i)
 			? FullSelection
 			: TextSelection();
-		auto corners = GetCornersFromSides(element.sides);
+		auto corners = Ui::GetCornersFromSides(element.sides);
 		if (!isBubbleTop()) {
 			corners &= ~(RectPart::TopLeft | RectPart::TopRight);
 		}
@@ -409,6 +395,23 @@ Storage::SharedMediaTypesMask HistoryGroupedMedia::sharedMediaTypes() const {
 	return main()->sharedMediaTypes();
 }
 
+void HistoryGroupedMedia::updateSentMedia(const MTPMessageMedia &media) {
+	return main()->updateSentMedia(media);
+}
+
+bool HistoryGroupedMedia::needReSetInlineResultMedia(
+		const MTPMessageMedia &media) {
+	return main()->needReSetInlineResultMedia(media);
+}
+
+PhotoData *HistoryGroupedMedia::getPhoto() const {
+	return main()->getPhoto();
+}
+
+DocumentData *HistoryGroupedMedia::getDocument() const {
+	return main()->getDocument();
+}
+
 HistoryMessageEdited *HistoryGroupedMedia::displayedEditBadge() const {
 	if (!_caption.isEmpty()) {
 		return _elements.front().item->Get<HistoryMessageEdited>();
diff --git a/Telegram/SourceFiles/history/history_media_grouped.h b/Telegram/SourceFiles/history/history_media_grouped.h
index bc856b9e7..fd4a6c065 100644
--- a/Telegram/SourceFiles/history/history_media_grouped.h
+++ b/Telegram/SourceFiles/history/history_media_grouped.h
@@ -34,14 +34,14 @@ public:
 		return MediaTypeGrouped;
 	}
 	std::unique_ptr<HistoryMedia> clone(
-			not_null<HistoryItem*> newParent,
-			not_null<HistoryItem*> realParent) const override {
-		return main()->clone(newParent, realParent);
-	}
+		not_null<HistoryItem*> newParent,
+		not_null<HistoryItem*> realParent) const override;
 
 	void initDimensions() override;
 	int resizeGetHeight(int width) override;
 	void refreshParentId(not_null<HistoryItem*> realParent) override;
+	void updateSentMedia(const MTPMessageMedia &media) override;
+	bool needReSetInlineResultMedia(const MTPMessageMedia &media) override;
 
 	void draw(
 		Painter &p,
@@ -66,12 +66,8 @@ public:
 		return !_caption.isEmpty();
 	}
 
-	PhotoData *getPhoto() const override {
-		return main()->getPhoto();
-	}
-	DocumentData *getDocument() const override {
-		return main()->getDocument();
-	}
+	PhotoData *getPhoto() const override;
+	DocumentData *getDocument() const override;
 
 	QString notificationText() const override;
 	QString inDialogsText() const override;
diff --git a/Telegram/SourceFiles/history/history_media_types.cpp b/Telegram/SourceFiles/history/history_media_types.cpp
index 0e027e23f..a6f46ab40 100644
--- a/Telegram/SourceFiles/history/history_media_types.cpp
+++ b/Telegram/SourceFiles/history/history_media_types.cpp
@@ -40,6 +40,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "styles/style_history.h"
 #include "calls/calls_instance.h"
 #include "ui/empty_userpic.h"
+#include "ui/grouped_layout.h"
 
 namespace {
 
@@ -118,33 +119,6 @@ int32 gifMaxStatusWidth(DocumentData *document) {
 	return result;
 }
 
-QSize CountPixSizeForSize(QSize original, QSize geometry) {
-	const auto width = geometry.width();
-	const auto height = geometry.height();
-	auto tw = original.width();
-	auto th = original.height();
-	if (tw * height > th * width) {
-		if (th > height || tw * height < 2 * th * width) {
-			tw = (height * tw) / th;
-			th = height;
-		} else if (tw < width) {
-			th = (width * th) / tw;
-			tw = width;
-		}
-	} else {
-		if (tw > width || th * width < 2 * tw * height) {
-			th = (width * th) / tw;
-			tw = width;
-		} else if (tw > 0 && th < height) {
-			tw = (height * tw) / th;
-			th = height;
-		}
-	}
-	if (tw < 1) tw = 1;
-	if (th < 1) th = 1;
-	return { tw, th };
-}
-
 } // namespace
 
 void HistoryInitMedia() {
@@ -726,7 +700,7 @@ void HistoryPhoto::validateGroupedCache(
 
 	const auto originalWidth = convertScale(_data->full->width());
 	const auto originalHeight = convertScale(_data->full->height());
-	const auto pixSize = CountPixSizeForSize(
+	const auto pixSize = Ui::GetImageScaleSizeForGeometry(
 		{ originalWidth, originalHeight },
 		{ width, height });
 	const auto pixWidth = pixSize.width() * cIntRetinaFactor();
@@ -1252,7 +1226,7 @@ void HistoryVideo::validateGroupedCache(
 
 	const auto originalWidth = convertScale(_data->thumb->width());
 	const auto originalHeight = convertScale(_data->thumb->height());
-	const auto pixSize = CountPixSizeForSize(
+	const auto pixSize = Ui::GetImageScaleSizeForGeometry(
 		{ originalWidth, originalHeight },
 		{ width, height });
 	const auto pixWidth = pixSize.width();
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index e7d46e853..62cee01d0 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -61,17 +61,18 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "mainwindow.h"
 #include "passcodewidget.h"
 #include "mainwindow.h"
+#include "storage/localimageloader.h"
+#include "storage/localstorage.h"
 #include "storage/file_upload.h"
+#include "storage/storage_media_prepare.h"
 #include "media/media_audio.h"
 #include "media/media_audio_capture.h"
 #include "media/player/media_player_instance.h"
-#include "storage/localstorage.h"
 #include "apiwrap.h"
 #include "history/history_top_bar_widget.h"
 #include "observer_peer.h"
 #include "base/qthelp_regex.h"
 #include "ui/widgets/popup_menu.h"
-#include "platform/platform_file_utilities.h"
 #include "auth_session.h"
 #include "window/themes/window_theme.h"
 #include "window/notifications_manager.h"
@@ -104,36 +105,6 @@ ApiWrap::RequestMessageDataCallback replyEditMessageDataCallback() {
 	};
 }
 
-MTPVector<MTPDocumentAttribute> composeDocumentAttributes(DocumentData *document) {
-	auto filenameAttribute = MTP_documentAttributeFilename(
-		MTP_string(document->filename()));
-	auto attributes = QVector<MTPDocumentAttribute>(1, filenameAttribute);
-	if (document->dimensions.width() > 0 && document->dimensions.height() > 0) {
-		int32 duration = document->duration();
-		if (duration >= 0) {
-			auto flags = MTPDdocumentAttributeVideo::Flags(0);
-			if (document->isVideoMessage()) {
-				flags |= MTPDdocumentAttributeVideo::Flag::f_round_message;
-			}
-			attributes.push_back(MTP_documentAttributeVideo(MTP_flags(flags), MTP_int(duration), MTP_int(document->dimensions.width()), MTP_int(document->dimensions.height())));
-		} else {
-			attributes.push_back(MTP_documentAttributeImageSize(MTP_int(document->dimensions.width()), MTP_int(document->dimensions.height())));
-		}
-	}
-	if (document->type == AnimatedDocument) {
-		attributes.push_back(MTP_documentAttributeAnimated());
-	} else if (document->type == StickerDocument && document->sticker()) {
-		attributes.push_back(MTP_documentAttributeSticker(MTP_flags(0), MTP_string(document->sticker()->alt), document->sticker()->set, MTPMaskCoords()));
-	} else if (const auto song = document->song()) {
-		auto flags = MTPDdocumentAttributeAudio::Flag::f_title | MTPDdocumentAttributeAudio::Flag::f_performer;
-		attributes.push_back(MTP_documentAttributeAudio(MTP_flags(flags), MTP_int(song->duration), MTP_string(song->title), MTP_string(song->performer), MTPstring()));
-	} else if (const auto voice = document->voice()) {
-		auto flags = MTPDdocumentAttributeAudio::Flag::f_voice | MTPDdocumentAttributeAudio::Flag::f_waveform;
-		attributes.push_back(MTP_documentAttributeAudio(MTP_flags(flags), MTP_int(voice->duration), MTPstring(), MTPstring(), MTP_bytes(documentWaveformEncode5bit(voice->waveform))));
-	}
-	return MTP_vector<MTPDocumentAttribute>(attributes);
-}
-
 } // namespace
 
 ReportSpamPanel::ReportSpamPanel(QWidget *parent) : TWidget(parent),
@@ -448,9 +419,9 @@ HistoryWidget::HistoryWidget(QWidget *parent, not_null<Window::Controller*> cont
 , _kbScroll(this, st::botKbScroll)
 , _tabbedPanel(this, controller)
 , _tabbedSelector(_tabbedPanel->getSelector())
+, _attachDragState(DragState::None)
 , _attachDragDocument(this)
 , _attachDragPhoto(this)
-, _fileLoader(this, FileLoaderQueueStopTimeout)
 , _sendActionStopTimer([this] { cancelTypingAction(); })
 , _topShadow(this) {
 	setAcceptDrops(true);
@@ -1294,14 +1265,17 @@ void HistoryWidget::onRecordError() {
 	stopRecording(false);
 }
 
-void HistoryWidget::onRecordDone(QByteArray result, VoiceWaveform waveform, qint32 samples) {
+void HistoryWidget::onRecordDone(
+		QByteArray result,
+		VoiceWaveform waveform,
+		qint32 samples) {
 	if (!canWriteMessage() || result.isEmpty()) return;
 
 	App::wnd()->activateWindow();
-	auto duration = samples / Media::Player::kDefaultFrequency;
-	auto to = FileLoadTo(_peer->id, _peer->notifySilentPosts(), replyToId());
-	auto caption = QString();
-	_fileLoader.addTask(std::make_unique<FileLoadTask>(result, duration, waveform, to, caption));
+	const auto duration = samples / Media::Player::kDefaultFrequency;
+	auto options = ApiWrap::SendOptions(_history);
+	options.replyTo = replyToId();
+	Auth().api().sendVoiceMessage(result, waveform, duration, options);
 }
 
 void HistoryWidget::onRecordUpdate(quint16 level, qint32 samples) {
@@ -3129,19 +3103,21 @@ void HistoryWidget::chooseAttach() {
 			auto animated = false;
 			auto image = App::readImage(result.remoteContent, nullptr, false, &animated);
 			if (!image.isNull() && !animated) {
-				confirmSendingFiles(image, result.remoteContent);
+				confirmSendingFiles(
+					image,
+					result.remoteContent,
+					CompressConfirm::Auto);
 			} else {
 				uploadFile(result.remoteContent, SendMediaType::File);
 			}
 		} else {
-			auto lists = getSendingFilesLists(result.paths);
-			if (lists.allFilesForCompress) {
-				confirmSendingFiles(lists);
-			} else {
-				validateSendingFiles(lists, [this](const QStringList &files) {
-					uploadFiles(files, SendMediaType::File);
-					return true;
-				});
+			auto list = Storage::PrepareMediaList(
+				result.paths,
+				st::sendMediaPreviewSize);
+			if (list.allFilesForCompress) {
+				confirmSendingFiles(std::move(list), CompressConfirm::Auto);
+			} else if (!showSendingFilesError(list)) {
+				uploadFiles(std::move(list), SendMediaType::File);
 			}
 		}
 	}));
@@ -3159,25 +3135,25 @@ void HistoryWidget::sendButtonClicked() {
 void HistoryWidget::dragEnterEvent(QDragEnterEvent *e) {
 	if (!_history || !_canSendMessages) return;
 
-	_attachDrag = getDragState(e->mimeData());
+	_attachDragState = Storage::ComputeMimeDataState(e->mimeData());
 	updateDragAreas();
 
-	if (_attachDrag != DragState::None) {
+	if (_attachDragState != DragState::None) {
 		e->setDropAction(Qt::IgnoreAction);
 		e->accept();
 	}
 }
 
 void HistoryWidget::dragLeaveEvent(QDragLeaveEvent *e) {
-	if (_attachDrag != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
-		_attachDrag = DragState::None;
+	if (_attachDragState != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
+		_attachDragState = DragState::None;
 		updateDragAreas();
 	}
 }
 
 void HistoryWidget::leaveEventHook(QEvent *e) {
-	if (_attachDrag != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
-		_attachDrag = DragState::None;
+	if (_attachDragState != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
+		_attachDragState = DragState::None;
 		updateDragAreas();
 	}
 	if (hasMouseTracking()) mouseMoveEvent(0);
@@ -3246,8 +3222,8 @@ void HistoryWidget::mouseReleaseEvent(QMouseEvent *e) {
 		_replyForwardPressed = false;
 		update(0, _field->y() - st::historySendPadding - st::historyReplyHeight, width(), st::historyReplyHeight);
 	}
-	if (_attachDrag != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
-		_attachDrag = DragState::None;
+	if (_attachDragState != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
+		_attachDragState = DragState::None;
 		updateDragAreas();
 	}
 	if (_recording) {
@@ -3477,60 +3453,11 @@ QRect HistoryWidget::rectForFloatPlayer() const {
 	return mapToGlobal(_scroll->geometry());
 }
 
-DragState HistoryWidget::getDragState(const QMimeData *d) {
-	if (!d
-		|| d->hasFormat(qsl("application/x-td-forward-selected"))
-		|| d->hasFormat(qsl("application/x-td-forward-pressed"))
-		|| d->hasFormat(qsl("application/x-td-forward-pressed-link"))) return DragState::None;
-
-	if (d->hasImage()) return DragState::Image;
-
-	QString uriListFormat(qsl("text/uri-list"));
-	if (!d->hasFormat(uriListFormat)) return DragState::None;
-
-	QStringList imgExtensions(cImgExtensions()), files;
-
-	const QList<QUrl> &urls(d->urls());
-	if (urls.isEmpty()) return DragState::None;
-
-	bool allAreSmallImages = true;
-	for (QList<QUrl>::const_iterator i = urls.cbegin(), en = urls.cend(); i != en; ++i) {
-		if (!i->isLocalFile()) return DragState::None;
-
-		auto file = Platform::File::UrlToLocal(*i);
-
-		QFileInfo info(file);
-		if (info.isDir()) return DragState::None;
-
-		quint64 s = info.size();
-		if (s > App::kFileSizeLimit) {
-			return DragState::None;
-		}
-		if (allAreSmallImages) {
-			if (s > App::kImageSizeLimit) {
-				allAreSmallImages = false;
-			} else {
-				bool foundImageExtension = false;
-				for (QStringList::const_iterator j = imgExtensions.cbegin(), end = imgExtensions.cend(); j != end; ++j) {
-					if (file.right(j->size()).toLower() == (*j).toLower()) {
-						foundImageExtension = true;
-						break;
-					}
-				}
-				if (!foundImageExtension) {
-					allAreSmallImages = false;
-				}
-			}
-		}
-	}
-	return allAreSmallImages ? DragState::PhotoFiles : DragState::Files;
-}
-
 void HistoryWidget::updateDragAreas() {
-	_field->setAcceptDrops(_attachDrag == DragState::None);
+	_field->setAcceptDrops(_attachDragState == DragState::None);
 	updateControlsGeometry();
 
-	switch (_attachDrag) {
+	switch (_attachDragState) {
 	case DragState::None:
 		_attachDragDocument->otherLeave();
 		_attachDragPhoto->otherLeave();
@@ -3670,7 +3597,7 @@ bool HistoryWidget::kbWasHidden() const {
 }
 
 void HistoryWidget::dropEvent(QDropEvent *e) {
-	_attachDrag = DragState::None;
+	_attachDragState = DragState::None;
 	updateDragAreas();
 	e->acceptProposedAction();
 }
@@ -4006,11 +3933,20 @@ void HistoryWidget::updateFieldPlaceholder() {
 }
 
 template <typename SendCallback>
-bool HistoryWidget::showSendFilesBox(object_ptr<SendFilesBox> box, const QString &insertTextOnCancel, const QString *addedComment, SendCallback callback) {
+bool HistoryWidget::showSendFilesBox(
+		object_ptr<SendFilesBox> box,
+		const QString &insertTextOnCancel,
+		const QString *addedComment,
+		SendCallback callback) {
 	App::wnd()->activateWindow();
 
 	auto withComment = (addedComment != nullptr);
-	box->setConfirmedCallback(base::lambda_guarded(this, [this, withComment, sendCallback = std::move(callback)](const QStringList &files, const QImage &image, std::unique_ptr<FileLoadTask::MediaInformation> information, bool compressed, const QString &caption, bool ctrlShiftEnter) {
+	const auto confirmedCallback = [=, sendCallback = std::move(callback)](
+			Storage::PreparedList &&list,
+			const QImage &image,
+			bool compressed,
+			const QString &caption,
+			bool ctrlShiftEnter) {
 		if (!canWriteMessage()) return;
 
 		const auto replyTo = replyToId();
@@ -4019,13 +3955,14 @@ bool HistoryWidget::showSendFilesBox(object_ptr<SendFilesBox> box, const QString
 			onSend(ctrlShiftEnter);
 		}
 		sendCallback(
-			files,
+			std::move(list),
 			image,
-			std::move(information),
 			compressed,
 			caption,
 			replyTo);
-	}));
+	};
+	box->setConfirmedCallback(
+		base::lambda_guarded(this, std::move(confirmedCallback)));
 
 	if (withComment) {
 		auto was = _field->getTextWithTags();
@@ -4043,66 +3980,161 @@ bool HistoryWidget::showSendFilesBox(object_ptr<SendFilesBox> box, const QString
 	return true;
 }
 
-template <typename Callback>
-bool HistoryWidget::validateSendingFiles(const SendingFilesLists &lists, Callback callback) {
-	if (!canWriteMessage()) return false;
-
+bool HistoryWidget::showSendingFilesError(
+		const Storage::PreparedList &list) const {
 	App::wnd()->activateWindow();
-	if (!lists.nonLocalUrls.isEmpty()) {
-		Ui::show(Box<InformBox>(lng_send_image_empty(lt_name, lists.nonLocalUrls.front().toDisplayString())));
-	} else if (!lists.emptyFiles.isEmpty()) {
-		Ui::show(Box<InformBox>(lng_send_image_empty(lt_name, lists.emptyFiles.front())));
-	} else if (!lists.tooLargeFiles.isEmpty()) {
-		Ui::show(Box<InformBox>(lng_send_image_too_large(lt_name, lists.tooLargeFiles.front())));
-	} else if (!lists.filesToSend.isEmpty()) {
-		return callback(lists.filesToSend);
-	}
-	return false;
-}
-
-bool HistoryWidget::confirmSendingFiles(const QList<QUrl> &files, CompressConfirm compressed, const QString *addedComment) {
-	return confirmSendingFiles(getSendingFilesLists(files), compressed, addedComment);
-}
-
-bool HistoryWidget::confirmSendingFiles(const QStringList &files, CompressConfirm compressed, const QString *addedComment) {
-	return confirmSendingFiles(getSendingFilesLists(files), compressed, addedComment);
-}
-
-bool HistoryWidget::confirmSendingFiles(const SendingFilesLists &lists, CompressConfirm compressed, const QString *addedComment) {
-	if (auto megagroup = _peer ? _peer->asMegagroup() : nullptr) {
-		if (megagroup->restricted(ChannelRestriction::f_send_media)) {
-			Ui::show(Box<InformBox>(lang(lng_restricted_send_media)));
-			return false;
+	const auto text = [&] {
+		if (const auto megagroup = _peer ? _peer->asMegagroup() : nullptr) {
+			if (megagroup->restricted(ChannelRestriction::f_send_media)) {
+				return lang(lng_restricted_send_media);
+			}
+		} else if (!canWriteMessage()) {
+			return lang(lng_forward_send_files_cant);
 		}
+		using Error = Storage::PreparedList::Error;
+		switch (list.error) {
+		case Error::None: return QString();
+		case Error::EmptyFile:
+		case Error::Directory:
+		case Error::NonLocalUrl: return lng_send_image_empty(
+			lt_name,
+			list.errorData);
+		case Error::TooLargeFile: return lng_send_image_too_large(
+			lt_name,
+			list.errorData);
+		}
+		return lang(lng_forward_send_files_cant);
+	}();
+	if (text.isEmpty()) {
+		return false;
 	}
-	return validateSendingFiles(lists, [this, &lists, compressed, addedComment](const QStringList &files) {
-		auto insertTextOnCancel = QString();
-		auto sendCallback = [this](const QStringList &files, const QImage &image, std::unique_ptr<FileLoadTask::MediaInformation> information, bool compressed, const QString &caption, MsgId replyTo) {
-			auto type = compressed ? SendMediaType::Photo : SendMediaType::File;
-			uploadFilesAfterConfirmation(files, QByteArray(), image, std::move(information), type, caption);
+	Ui::show(Box<InformBox>(text));
+	return true;
+}
+
+bool HistoryWidget::confirmSendingFiles(const QStringList &files) {
+	return confirmSendingFiles(files, CompressConfirm::Auto, nullptr);
+}
+
+bool HistoryWidget::confirmSendingFiles(const QMimeData *data) {
+	return confirmSendingFiles(data, CompressConfirm::Auto, nullptr);
+}
+
+bool HistoryWidget::confirmSendingFiles(
+		const QList<QUrl> &files,
+		CompressConfirm compressed,
+		const QString *addedComment) {
+	return confirmSendingFiles(
+		Storage::PrepareMediaList(files, st::sendMediaPreviewSize),
+		compressed,
+		addedComment);
+}
+
+bool HistoryWidget::confirmSendingFiles(
+		const QStringList &files,
+		CompressConfirm compressed,
+		const QString *addedComment) {
+	return confirmSendingFiles(
+		Storage::PrepareMediaList(files, st::sendMediaPreviewSize),
+		compressed,
+		addedComment);
+}
+
+bool HistoryWidget::confirmSendingFiles(
+		Storage::PreparedList &&list,
+		CompressConfirm compressed,
+		const QString *addedComment) {
+	if (showSendingFilesError(list)) {
+		return false;
+	}
+	if (list.albumIsPossible) {
+		auto box = Ui::show(Box<SendAlbumBox>(std::move(list)));
+		const auto confirmedCallback = [=](
+				Storage::PreparedList &&list,
+				const QString &caption,
+				bool ctrlShiftEnter) {
+			if (!canWriteMessage()) return;
+
+			uploadFilesAfterConfirmation(
+				std::move(list),
+				QByteArray(),
+				QImage(),
+				SendMediaType::Photo,
+				caption,
+				replyToId(),
+				std::make_shared<SendingAlbum>());
 		};
-		auto boxCompressConfirm = compressed;
-		if (files.size() > 1 && !lists.allFilesForCompress) {
-			boxCompressConfirm = CompressConfirm::None;
-		}
-		auto box = Box<SendFilesBox>(files, boxCompressConfirm);
-		return showSendFilesBox(std::move(box), insertTextOnCancel, addedComment, std::move(sendCallback));
-	});
+		box->setConfirmedCallback(
+			base::lambda_guarded(this, std::move(confirmedCallback)));
+		return true;
+	} else {
+		const auto insertTextOnCancel = QString();
+		auto sendCallback = [this](
+				Storage::PreparedList &&list,
+				const QImage &image,
+				bool compressed,
+				const QString &caption,
+				MsgId replyTo) {
+			const auto type = compressed
+				? SendMediaType::Photo
+				: SendMediaType::File;
+			uploadFilesAfterConfirmation(
+				std::move(list),
+				QByteArray(),
+				image,
+				type,
+				caption,
+				replyTo);
+		};
+		const auto noCompressOption = (list.files.size() > 1)
+			&& !list.allFilesForCompress;
+		const auto boxCompressConfirm = noCompressOption
+			? CompressConfirm::None
+			: compressed;
+		return showSendFilesBox(
+			Box<SendFilesBox>(std::move(list), boxCompressConfirm),
+			insertTextOnCancel,
+			addedComment,
+			std::move(sendCallback));
+	}
 }
 
-bool HistoryWidget::confirmSendingFiles(const QImage &image, const QByteArray &content, CompressConfirm compressed, const QString &insertTextOnCancel) {
+bool HistoryWidget::confirmSendingFiles(
+		const QImage &image,
+		const QByteArray &content,
+		CompressConfirm compressed,
+		const QString &insertTextOnCancel) {
 	if (!canWriteMessage() || image.isNull()) return false;
 
 	App::wnd()->activateWindow();
-	auto sendCallback = [this, content](const QStringList &files, const QImage &image, std::unique_ptr<FileLoadTask::MediaInformation> information, bool compressed, const QString &caption, MsgId replyTo) {
-		auto type = compressed ? SendMediaType::Photo : SendMediaType::File;
-		uploadFilesAfterConfirmation(files, content, image, std::move(information), type, caption);
+	auto sendCallback = [this, content](
+			Storage::PreparedList &&list,
+			const QImage &image,
+			bool compressed,
+			const QString &caption,
+			MsgId replyTo) {
+		const auto type = compressed
+			? SendMediaType::Photo
+			: SendMediaType::File;
+		uploadFilesAfterConfirmation(
+			std::move(list),
+			content,
+			image,
+			type,
+			caption,
+			replyTo);
 	};
-	auto box = Box<SendFilesBox>(image, compressed);
-	return showSendFilesBox(std::move(box), insertTextOnCancel, nullptr, std::move(sendCallback));
+	return showSendFilesBox(
+		Box<SendFilesBox>(image, compressed),
+		insertTextOnCancel,
+		nullptr,
+		std::move(sendCallback));
 }
 
-bool HistoryWidget::confirmSendingFiles(const QMimeData *data, CompressConfirm compressed, const QString &insertTextOnCancel) {
+bool HistoryWidget::confirmSendingFiles(
+		const QMimeData *data,
+		CompressConfirm compressed,
+		const QString &insertTextOnCancel) {
 	if (!canWriteMessage()) {
 		return false;
 	}
@@ -4119,7 +4151,11 @@ bool HistoryWidget::confirmSendingFiles(const QMimeData *data, CompressConfirm c
 	if (data->hasImage()) {
 		auto image = qvariant_cast<QImage>(data->imageData());
 		if (!image.isNull()) {
-			confirmSendingFiles(image, QByteArray(), compressed, insertTextOnCancel);
+			confirmSendingFiles(
+				image,
+				QByteArray(),
+				compressed,
+				insertTextOnCancel);
 			return true;
 		}
 	}
@@ -4133,11 +4169,9 @@ bool HistoryWidget::confirmShareContact(
 		const QString *addedComment) {
 	if (!canWriteMessage()) return false;
 
-	auto box = Box<SendFilesBox>(phone, fname, lname);
 	auto sendCallback = [=](
-			const QStringList &files,
+			Storage::PreparedList &&list,
 			const QImage &image,
-			std::unique_ptr<FileLoadTask::MediaInformation> information,
 			bool compressed,
 			const QString &caption,
 			MsgId replyTo) {
@@ -4145,113 +4179,79 @@ bool HistoryWidget::confirmShareContact(
 		options.replyTo = replyTo;
 		Auth().api().shareContact(phone, fname, lname, options);
 	};
-	auto insertTextOnCancel = QString();
+	const auto insertTextOnCancel = QString();
 	return showSendFilesBox(
-		std::move(box),
+		Box<SendFilesBox>(phone, fname, lname),
 		insertTextOnCancel,
 		addedComment,
 		std::move(sendCallback));
 }
 
-HistoryWidget::SendingFilesLists HistoryWidget::getSendingFilesLists(const QList<QUrl> &files) {
-	auto result = SendingFilesLists();
-	for_const (auto &url, files) {
-		if (!url.isLocalFile()) {
-			result.nonLocalUrls.push_back(url);
-		} else {
-			auto filepath = Platform::File::UrlToLocal(url);
-			getSendingLocalFileInfo(result, filepath);
-		}
-	}
-	return result;
-}
-
-HistoryWidget::SendingFilesLists HistoryWidget::getSendingFilesLists(const QStringList &files) {
-	auto result = SendingFilesLists();
-	for_const (auto &filepath, files) {
-		getSendingLocalFileInfo(result, filepath);
-	}
-	return result;
-}
-
-void HistoryWidget::getSendingLocalFileInfo(SendingFilesLists &result, const QString &filepath) {
-	auto hasExtensionForCompress = [](const QString &filepath) {
-		for_const (auto extension, cExtensionsForCompress()) {
-			if (filepath.right(extension.size()).compare(extension, Qt::CaseInsensitive) == 0) {
-				return true;
-			}
-		}
-		return false;
-	};
-	auto fileinfo = QFileInfo(filepath);
-	if (fileinfo.isDir()) {
-		result.directories.push_back(filepath);
-	} else {
-		auto filesize = fileinfo.size();
-		if (filesize <= 0) {
-			result.emptyFiles.push_back(filepath);
-		} else if (filesize > App::kFileSizeLimit) {
-			result.tooLargeFiles.push_back(filepath);
-		} else {
-			result.filesToSend.push_back(filepath);
-			if (result.allFilesForCompress) {
-				if (filesize > App::kImageSizeLimit || !hasExtensionForCompress(filepath)) {
-					result.allFilesForCompress = false;
-				}
-			}
-		}
-	}
-}
-
-void HistoryWidget::uploadFiles(const QStringList &files, SendMediaType type) {
+void HistoryWidget::uploadFiles(
+		Storage::PreparedList &&list,
+		SendMediaType type) {
 	if (!canWriteMessage()) return;
 
 	auto caption = QString();
-	uploadFilesAfterConfirmation(files, QByteArray(), QImage(), nullptr, type, caption);
+	uploadFilesAfterConfirmation(
+		std::move(list),
+		QByteArray(),
+		QImage(),
+		type,
+		caption,
+		replyToId());
 }
 
 void HistoryWidget::uploadFilesAfterConfirmation(
-		const QStringList &files,
+		Storage::PreparedList &&list,
 		const QByteArray &content,
 		const QImage &image,
-		std::unique_ptr<FileLoadTask::MediaInformation> information,
 		SendMediaType type,
-		QString caption) {
+		QString caption,
+		MsgId replyTo,
+		std::shared_ptr<SendingAlbum> album) {
 	Assert(canWriteMessage());
 
-	auto to = FileLoadTo(_peer->id, _peer->notifySilentPosts(), replyToId());
-	if (files.size() > 1 && !caption.isEmpty()) {
-		auto message = MainWidget::MessageToSend(_history);
-		message.textWithTags = { caption, TextWithTags::Tags() };
-		message.replyTo = to.replyTo;
-		message.clearDraft = false;
-		App::main()->sendMessage(message);
-		caption = QString();
-	}
-	auto tasks = std::vector<std::unique_ptr<Task>>();
-	tasks.reserve(files.size());
-	for_const (auto &filepath, files) {
-		if (filepath.isEmpty() && (!image.isNull() || !content.isNull())) {
-			tasks.push_back(std::make_unique<FileLoadTask>(content, image, type, to, caption));
-		} else {
-			tasks.push_back(std::make_unique<FileLoadTask>(filepath, std::move(information), type, to, caption));
-		}
-	}
-	_fileLoader.addTasks(std::move(tasks));
+	auto options = ApiWrap::SendOptions(_history);
+	options.replyTo = replyTo;
+	Auth().api().sendFiles(
+		std::move(list),
+		content,
+		image,
+		type,
+		caption,
+		album,
+		options);
 }
 
-void HistoryWidget::uploadFile(const QByteArray &fileContent, SendMediaType type) {
+void HistoryWidget::uploadFile(
+		const QByteArray &fileContent,
+		SendMediaType type) {
 	if (!canWriteMessage()) return;
 
-	auto to = FileLoadTo(_peer->id, _peer->notifySilentPosts(), replyToId());
-	auto caption = QString();
-	_fileLoader.addTask(std::make_unique<FileLoadTask>(fileContent, QImage(), type, to, caption));
+	auto options = ApiWrap::SendOptions(_history);
+	options.replyTo = replyToId();
+	Auth().api().sendFile(fileContent, type, options);
 }
 
-void HistoryWidget::sendFileConfirmed(const FileLoadResultPtr &file) {
-	bool lastKeyboardUsed = lastForceReplyReplied(FullMsgId(peerToChannel(file->to.peer), file->to.replyTo));
+void HistoryWidget::sendFileConfirmed(
+		const std::shared_ptr<FileLoadResult> &file) {
+	const auto channelId = peerToChannel(file->to.peer);
+	const auto lastKeyboardUsed = lastForceReplyReplied(FullMsgId(
+		channelId,
+		file->to.replyTo));
 
-	FullMsgId newId(peerToChannel(file->to.peer), clientMsgId());
+	const auto newId = FullMsgId(channelId, clientMsgId());
+	const auto groupId = file->album ? file->album->groupId : uint64(0);
+	if (file->album) {
+		const auto proj = [](const SendingAlbum::Item &item) {
+			return item.taskId;
+		};
+		const auto it = ranges::find(file->album->items, file->taskId, proj);
+		Assert(it != file->album->items.end());
+
+		it->msgId = newId;
+	}
 
 	connect(&Auth().uploader(), SIGNAL(photoReady(const FullMsgId&,bool,const MTPInputFile&)), this, SLOT(onPhotoUploaded(const FullMsgId&,bool,const MTPInputFile&)), Qt::UniqueConnection);
 	connect(&Auth().uploader(), SIGNAL(documentReady(const FullMsgId&,bool,const MTPInputFile&)), this, SLOT(onDocumentUploaded(const FullMsgId&,bool,const MTPInputFile&)), Qt::UniqueConnection);
@@ -4288,6 +4288,9 @@ void HistoryWidget::sendFileConfirmed(const FileLoadResultPtr &file) {
 	if (silentPost) {
 		flags |= MTPDmessage::Flag::f_silent;
 	}
+	if (groupId) {
+		flags |= MTPDmessage::Flag::f_grouped_id;
+	}
 	auto messageFromId = channelPost ? 0 : Auth().userId();
 	auto messagePostAuthor = channelPost ? (Auth().user()->firstName + ' ' + Auth().user()->lastName) : QString();
 	if (file->type == SendMediaType::Photo) {
@@ -4317,7 +4320,7 @@ void HistoryWidget::sendFileConfirmed(const FileLoadResultPtr &file) {
 				MTP_int(1),
 				MTPint(),
 				MTP_string(messagePostAuthor),
-				MTPlong()),
+				MTP_long(groupId)),
 			NewMessageUnread);
 	} else if (file->type == SendMediaType::File) {
 		auto documentFlags = MTPDmessageMediaDocument::Flag::f_document | 0;
@@ -4346,7 +4349,7 @@ void HistoryWidget::sendFileConfirmed(const FileLoadResultPtr &file) {
 				MTP_int(1),
 				MTPint(),
 				MTP_string(messagePostAuthor),
-				MTPlong()),
+				MTP_long(groupId)),
 			NewMessageUnread);
 	} else if (file->type == SendMediaType::Audio) {
 		if (!peer->isChannel()) {
@@ -4378,7 +4381,7 @@ void HistoryWidget::sendFileConfirmed(const FileLoadResultPtr &file) {
 				MTP_int(1),
 				MTPint(),
 				MTP_string(messagePostAuthor),
-				MTPlong()),
+				MTP_long(groupId)),
 			NewMessageUnread);
 	}
 
@@ -4393,90 +4396,14 @@ void HistoryWidget::onPhotoUploaded(
 		const FullMsgId &newId,
 		bool silent,
 		const MTPInputFile &file) {
-	if (auto item = App::histItemById(newId)) {
-		uint64 randomId = rand_value<uint64>();
-		App::historyRegRandom(randomId, newId);
-		History *hist = item->history();
-		MsgId replyTo = item->replyToId();
-		auto sendFlags = MTPmessages_SendMedia::Flags(0);
-		if (replyTo) {
-			sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
-		}
-
-		bool channelPost = hist->peer->isChannel() && !hist->peer->isMegagroup();
-		bool silentPost = channelPost && silent;
-		if (silentPost) {
-			sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
-		}
-		auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities();
-		auto media = MTP_inputMediaUploadedPhoto(
-			MTP_flags(0),
-			file,
-			MTP_string(caption.text),
-			MTPVector<MTPInputDocument>(),
-			MTP_int(0));
-		hist->sendRequestId = MTP::send(
-			MTPmessages_SendMedia(
-				MTP_flags(sendFlags),
-				item->history()->peer->input,
-				MTP_int(replyTo),
-				media,
-				MTP_long(randomId),
-				MTPnullMarkup),
-			App::main()->rpcDone(&MainWidget::sentUpdatesReceived),
-			App::main()->rpcFail(&MainWidget::sendMessageFail),
-			0,
-			0,
-			hist->sendRequestId);
-	}
+	Auth().api().sendUploadedPhoto(newId, file, silent);
 }
 
 void HistoryWidget::onDocumentUploaded(
 		const FullMsgId &newId,
 		bool silent,
 		const MTPInputFile &file) {
-	if (auto item = dynamic_cast<HistoryMessage*>(App::histItemById(newId))) {
-		auto media = item->getMedia();
-		if (auto document = media ? media->getDocument() : nullptr) {
-			auto randomId = rand_value<uint64>();
-			App::historyRegRandom(randomId, newId);
-			auto hist = item->history();
-			auto replyTo = item->replyToId();
-			auto sendFlags = MTPmessages_SendMedia::Flags(0);
-			if (replyTo) {
-				sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
-			}
-
-			bool channelPost = hist->peer->isChannel() && !hist->peer->isMegagroup();
-			bool silentPost = channelPost && silent;
-			if (silentPost) {
-				sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
-			}
-			auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities();
-			auto media = MTP_inputMediaUploadedDocument(
-				MTP_flags(0),
-				file,
-				MTPInputFile(),
-				MTP_string(document->mimeString()),
-				composeDocumentAttributes(document),
-				MTP_string(caption.text),
-				MTPVector<MTPInputDocument>(),
-				MTP_int(0));
-			hist->sendRequestId = MTP::send(
-				MTPmessages_SendMedia(
-					MTP_flags(sendFlags),
-					item->history()->peer->input,
-					MTP_int(replyTo),
-					media,
-					MTP_long(randomId),
-					MTPnullMarkup),
-				App::main()->rpcDone(&MainWidget::sentUpdatesReceived),
-				App::main()->rpcFail(&MainWidget::sendMessageFail),
-				0,
-				0,
-				hist->sendRequestId);
-		}
-	}
+	Auth().api().sendUploadedDocument(newId, file, base::none, silent);
 }
 
 void HistoryWidget::onThumbDocumentUploaded(
@@ -4484,48 +4411,7 @@ void HistoryWidget::onThumbDocumentUploaded(
 		bool silent,
 		const MTPInputFile &file,
 		const MTPInputFile &thumb) {
-	if (auto item = dynamic_cast<HistoryMessage*>(App::histItemById(newId))) {
-		auto media = item->getMedia();
-		if (auto document = media ? media->getDocument() : nullptr) {
-			auto randomId = rand_value<uint64>();
-			App::historyRegRandom(randomId, newId);
-			auto hist = item->history();
-			auto replyTo = item->replyToId();
-			auto sendFlags = MTPmessages_SendMedia::Flags(0);
-			if (replyTo) {
-				sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
-			}
-
-			bool channelPost = hist->peer->isChannel() && !hist->peer->isMegagroup();
-			bool silentPost = channelPost && silent;
-			if (silentPost) {
-				sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
-			}
-			auto caption = media ? media->getCaption() : TextWithEntities();
-			auto media = MTP_inputMediaUploadedDocument(
-				MTP_flags(MTPDinputMediaUploadedDocument::Flag::f_thumb),
-				file,
-				thumb,
-				MTP_string(document->mimeString()),
-				composeDocumentAttributes(document),
-				MTP_string(caption.text),
-				MTPVector<MTPInputDocument>(),
-				MTP_int(0));
-			hist->sendRequestId = MTP::send(
-				MTPmessages_SendMedia(
-					MTP_flags(sendFlags),
-					item->history()->peer->input,
-					MTP_int(replyTo),
-					media,
-					MTP_long(randomId),
-					MTPnullMarkup),
-				App::main()->rpcDone(&MainWidget::sentUpdatesReceived),
-				App::main()->rpcFail(&MainWidget::sendMessageFail),
-				0,
-				0,
-				hist->sendRequestId);
-		}
-	}
+	Auth().api().sendUploadedDocument(newId, file, thumb, silent);
 }
 
 void HistoryWidget::onPhotoProgress(const FullMsgId &newId) {
@@ -4756,7 +4642,7 @@ void HistoryWidget::updateControlsGeometry() {
 		_membersDropdown->setMaxHeight(countMembersDropdownHeightMax());
 	}
 
-	switch (_attachDrag) {
+	switch (_attachDragState) {
 	case DragState::Files:
 		_attachDragDocument->resize(width() - st::dragMargin.left() - st::dragMargin.right(), height() - st::dragMargin.top() - st::dragMargin.bottom());
 		_attachDragDocument->move(st::dragMargin.left(), st::dragMargin.top());
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index 58a08ddb2..d8b69ed8e 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -20,7 +20,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #pragma once
 
-#include "storage/localimageloader.h"
 #include "ui/widgets/tooltip.h"
 #include "mainwidget.h"
 #include "chat_helpers/field_autocomplete.h"
@@ -31,6 +30,12 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "base/flags.h"
 #include "base/timer.h"
 
+struct FileLoadResult;
+struct FileMediaInformation;
+struct SendingAlbum;
+enum class SendMediaType;
+enum class CompressConfirm;
+
 namespace InlineBots {
 namespace Layout {
 class ItemBase;
@@ -68,6 +73,11 @@ class TabbedSection;
 class TabbedSelector;
 } // namespace ChatHelpers
 
+namespace Storage {
+enum class MimeDataState;
+struct PreparedList;
+} // namespace Storage
+
 class DragArea;
 class SendFilesBox;
 class BotKeyboard;
@@ -215,16 +225,9 @@ public:
 	void updateFieldPlaceholder();
 	void updateStickersByEmoji();
 
-	bool confirmSendingFiles(const QList<QUrl> &files, CompressConfirm compressed = CompressConfirm::Auto, const QString *addedComment = nullptr);
-	bool confirmSendingFiles(const QStringList &files, CompressConfirm compressed = CompressConfirm::Auto, const QString *addedComment = nullptr);
-	bool confirmSendingFiles(const QImage &image, const QByteArray &content, CompressConfirm compressed = CompressConfirm::Auto, const QString &insertTextOnCancel = QString());
-	bool confirmSendingFiles(const QMimeData *data, CompressConfirm compressed = CompressConfirm::Auto, const QString &insertTextOnCancel = QString());
-	bool confirmShareContact(const QString &phone, const QString &fname, const QString &lname, const QString *addedComment = nullptr);
-
-	void uploadFile(const QByteArray &fileContent, SendMediaType type);
-	void uploadFiles(const QStringList &files, SendMediaType type);
-
-	void sendFileConfirmed(const FileLoadResultPtr &file);
+	bool confirmSendingFiles(const QStringList &files);
+	bool confirmSendingFiles(const QMimeData *data);
+	void sendFileConfirmed(const std::shared_ptr<FileLoadResult> &file);
 
 	void updateControlsVisibility();
 	void updateControlsGeometry();
@@ -291,8 +294,6 @@ public:
 	// already shown for the passed history item.
 	void updateBotKeyboard(History *h = nullptr, bool force = false);
 
-	DragState getDragState(const QMimeData *d);
-
 	void fastShowAtEnd(not_null<History*> history);
 	void applyDraft(bool parseLinks = true, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction = Ui::FlatTextarea::ClearUndoHistory);
 	void showHistory(const PeerId &peer, MsgId showAtMsgId, bool reload = false);
@@ -452,16 +453,9 @@ private slots:
 	void updateField();
 
 private:
-	struct SendingFilesLists {
-		QList<QUrl> nonLocalUrls;
-		QStringList directories;
-		QStringList emptyFiles;
-		QStringList tooLargeFiles;
-		QStringList filesToSend;
-		bool allFilesForCompress = true;
-	};
 	using TabbedPanel = ChatHelpers::TabbedPanel;
 	using TabbedSelector = ChatHelpers::TabbedSelector;
+	using DragState = Storage::MimeDataState;
 
 	void repaintHistoryItem(not_null<const HistoryItem*> item);
 	void handlePendingHistoryUpdate();
@@ -502,17 +496,29 @@ private:
 	void historyDownAnimationFinish();
 	void unreadMentionsAnimationFinish();
 	void sendButtonClicked();
-	SendingFilesLists getSendingFilesLists(const QList<QUrl> &files);
-	SendingFilesLists getSendingFilesLists(const QStringList &files);
-	void getSendingLocalFileInfo(SendingFilesLists &result, const QString &filepath);
-	bool confirmSendingFiles(const SendingFilesLists &lists, CompressConfirm compressed = CompressConfirm::Auto, const QString *addedComment = nullptr);
-	template <typename Callback>
-	bool validateSendingFiles(const SendingFilesLists &lists, Callback callback);
+
+	bool confirmShareContact(const QString &phone, const QString &fname, const QString &lname, const QString *addedComment = nullptr);
+	bool confirmSendingFiles(const QList<QUrl> &files, CompressConfirm compressed, const QString *addedComment = nullptr);
+	bool confirmSendingFiles(const QStringList &files, CompressConfirm compressed, const QString *addedComment = nullptr);
+	bool confirmSendingFiles(const QImage &image, const QByteArray &content, CompressConfirm compressed, const QString &insertTextOnCancel = QString());
+	bool confirmSendingFiles(const QMimeData *data, CompressConfirm compressed, const QString &insertTextOnCancel = QString());
+	bool confirmSendingFiles(Storage::PreparedList &&list, CompressConfirm compressed, const QString *addedComment = nullptr);
+	bool showSendingFilesError(const Storage::PreparedList &list) const;
 	template <typename SendCallback>
 	bool showSendFilesBox(object_ptr<SendFilesBox> box, const QString &insertTextOnCancel, const QString *addedComment, SendCallback callback);
 
+	void uploadFiles(Storage::PreparedList &&list, SendMediaType type);
+	void uploadFile(const QByteArray &fileContent, SendMediaType type);
+
 	// If an empty filepath is found we upload (possible) "image" with (possible) "content".
-	void uploadFilesAfterConfirmation(const QStringList &files, const QByteArray &content, const QImage &image, std::unique_ptr<FileLoadTask::MediaInformation> information, SendMediaType type, QString caption);
+	void uploadFilesAfterConfirmation(
+		Storage::PreparedList &&list,
+		const QByteArray &content,
+		const QImage &image,
+		SendMediaType type,
+		QString caption,
+		MsgId replyTo,
+		std::shared_ptr<SendingAlbum> album = nullptr);
 
 	void itemRemoved(not_null<const HistoryItem*> item);
 
@@ -826,14 +832,13 @@ private:
 	object_ptr<InlineBots::Layout::Widget> _inlineResults = { nullptr };
 	object_ptr<TabbedPanel> _tabbedPanel;
 	QPointer<TabbedSelector> _tabbedSelector;
-	DragState _attachDrag = DragState::None;
+	DragState _attachDragState;
 	object_ptr<DragArea> _attachDragDocument, _attachDragPhoto;
 
 	object_ptr<Ui::Emoji::SuggestionsController> _emojiSuggestions = { nullptr };
 
 	bool _nonEmptySelection = false;
 
-	TaskQueue _fileLoader;
 	TextUpdateEvents _textUpdateEvents = (TextUpdateEvents() | TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping);
 
 	int64 _serviceImageCacheSize = 0;
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index 342d4eb9d..8c4a8441e 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -1044,10 +1044,6 @@ void MainWidget::dialogsActivate() {
 	_dialogs->activate();
 }
 
-DragState MainWidget::getDragState(const QMimeData *mime) {
-	return _history->getDragState(mime);
-}
-
 bool MainWidget::leaveChatFailed(PeerData *peer, const RPCError &error) {
 	if (MTP::isDefaultHandledError(error)) return false;
 
@@ -1384,8 +1380,11 @@ bool MainWidget::sendMessageFail(const RPCError &error) {
 		Ui::show(Box<InformBox>(PeerFloodErrorText(PeerFloodType::Send)));
 		return true;
 	} else if (error.type() == qstr("USER_BANNED_IN_CHANNEL")) {
-		auto link = textcmdLink(Messenger::Instance().createInternalLinkFull(qsl("spambot")), lang(lng_cant_more_info));
-		Ui::show(Box<InformBox>(lng_error_public_groups_denied(lt_more_info, link)));
+		const auto link = textcmdLink(
+			Messenger::Instance().createInternalLinkFull(qsl("spambot")),
+			lang(lng_cant_more_info));
+		const auto text = lng_error_public_groups_denied(lt_more_info, link);
+		Ui::show(Box<InformBox>(text));
 		return true;
 	}
 	return false;
@@ -1977,7 +1976,8 @@ void MainWidget::mediaMarkRead(not_null<HistoryItem*> item) {
 	}
 }
 
-void MainWidget::onSendFileConfirm(const FileLoadResultPtr &file) {
+void MainWidget::onSendFileConfirm(
+		const std::shared_ptr<FileLoadResult> &file) {
 	_history->sendFileConfirmed(file);
 }
 
diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h
index 87a77f897..22585fcda 100644
--- a/Telegram/SourceFiles/mainwidget.h
+++ b/Telegram/SourceFiles/mainwidget.h
@@ -20,7 +20,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #pragma once
 
-#include "storage/localimageloader.h"
 #include "core/single_timer.h"
 #include "base/weak_ptr.h"
 #include "ui/rp_widget.h"
@@ -32,6 +31,7 @@ class DialogsWidget;
 class HistoryWidget;
 class HistoryHider;
 class StackItem;
+struct FileLoadResult;
 
 namespace Notify {
 struct PeerUpdate;
@@ -80,13 +80,6 @@ class ItemBase;
 } // namespace Layout
 } // namespace InlineBots
 
-enum class DragState {
-	None = 0x00,
-	Files = 0x01,
-	PhotoFiles = 0x02,
-	Image = 0x03,
-};
-
 class MainWidget : public Ui::RpWidget, public RPCSender, private base::Subscriber {
 	Q_OBJECT
 
@@ -166,7 +159,7 @@ public:
 	QPixmap grabForShowAnimation(const Window::SectionSlideParams &params);
 	void checkMainSectionToLayer();
 
-	void onSendFileConfirm(const FileLoadResultPtr &file);
+	void onSendFileConfirm(const std::shared_ptr<FileLoadResult> &file);
 	bool onSendSticker(DocumentData *sticker);
 
 	void destroyData();
@@ -208,8 +201,6 @@ public:
 
 	void deletePhotoLayer(PhotoData *photo);
 
-	DragState getDragState(const QMimeData *mime);
-
 	bool leaveChatFailed(PeerData *peer, const RPCError &e);
 	void deleteHistoryAfterLeave(PeerData *peer, const MTPUpdates &updates);
 	void deleteMessages(
diff --git a/Telegram/SourceFiles/media/media_audio.cpp b/Telegram/SourceFiles/media/media_audio.cpp
index bb937e17c..89d4302e1 100644
--- a/Telegram/SourceFiles/media/media_audio.cpp
+++ b/Telegram/SourceFiles/media/media_audio.cpp
@@ -1499,8 +1499,8 @@ private:
 namespace Media {
 namespace Player {
 
-FileLoadTask::Song PrepareForSending(const QString &fname, const QByteArray &data) {
-	auto result = FileLoadTask::Song();
+FileMediaInformation::Song PrepareForSending(const QString &fname, const QByteArray &data) {
+	auto result = FileMediaInformation::Song();
 	FFMpegAttributesReader reader(FileLocation(fname), data);
 	const auto positionMs = TimeMs(0);
 	if (reader.open(positionMs) && reader.samplesCount() > 0) {
diff --git a/Telegram/SourceFiles/media/media_audio.h b/Telegram/SourceFiles/media/media_audio.h
index 667473db9..1a9e5cb7c 100644
--- a/Telegram/SourceFiles/media/media_audio.h
+++ b/Telegram/SourceFiles/media/media_audio.h
@@ -310,7 +310,7 @@ private:
 
 };
 
-FileLoadTask::Song PrepareForSending(const QString &fname, const QByteArray &data);
+FileMediaInformation::Song PrepareForSending(const QString &fname, const QByteArray &data);
 
 namespace internal {
 
diff --git a/Telegram/SourceFiles/media/media_clip_reader.cpp b/Telegram/SourceFiles/media/media_clip_reader.cpp
index 5b6506443..7d637d5f7 100644
--- a/Telegram/SourceFiles/media/media_clip_reader.cpp
+++ b/Telegram/SourceFiles/media/media_clip_reader.cpp
@@ -872,8 +872,8 @@ Manager::~Manager() {
 	clear();
 }
 
-FileLoadTask::Video PrepareForSending(const QString &fname, const QByteArray &data) {
-	auto result = FileLoadTask::Video();
+FileMediaInformation::Video PrepareForSending(const QString &fname, const QByteArray &data) {
+	auto result = FileMediaInformation::Video();
 	auto localLocation = FileLocation(fname);
 	auto localData = QByteArray(data);
 
diff --git a/Telegram/SourceFiles/media/media_clip_reader.h b/Telegram/SourceFiles/media/media_clip_reader.h
index ceed55853..f4ad477d1 100644
--- a/Telegram/SourceFiles/media/media_clip_reader.h
+++ b/Telegram/SourceFiles/media/media_clip_reader.h
@@ -249,7 +249,7 @@ private:
 
 };
 
-FileLoadTask::Video PrepareForSending(const QString &fname, const QByteArray &data);
+FileMediaInformation::Video PrepareForSending(const QString &fname, const QByteArray &data);
 
 void Finish();
 
diff --git a/Telegram/SourceFiles/storage/file_upload.cpp b/Telegram/SourceFiles/storage/file_upload.cpp
index 74835edcd..65404231c 100644
--- a/Telegram/SourceFiles/storage/file_upload.cpp
+++ b/Telegram/SourceFiles/storage/file_upload.cpp
@@ -20,6 +20,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #include "storage/file_upload.h"
 
+#include "storage/localimageloader.h"
 #include "data/data_document.h"
 #include "data/data_photo.h"
 
@@ -30,6 +31,95 @@ constexpr auto kMaxUploadFileParallelSize = MTP::kUploadSessionsCount * 512 * 10
 
 } // namespace
 
+struct Uploader::File {
+	File(const SendMediaReady &media);
+	File(const std::shared_ptr<FileLoadResult> &file);
+
+	void setDocSize(int32 size);
+	bool setPartSize(uint32 partSize);
+
+	std::shared_ptr<FileLoadResult> file;
+	SendMediaReady media;
+	int32 partsCount;
+	mutable int32 fileSentSize;
+
+	uint64 id() const;
+	SendMediaType type() const;
+	uint64 thumbId() const;
+	const QString &filename() const;
+
+	HashMd5 md5Hash;
+
+	std::unique_ptr<QFile> docFile;
+	int32 docSentParts = 0;
+	int32 docSize = 0;
+	int32 docPartSize = 0;
+	int32 docPartsCount = 0;
+
+};
+
+Uploader::File::File(const SendMediaReady &media) : media(media) {
+	partsCount = media.parts.size();
+	if (type() == SendMediaType::File || type() == SendMediaType::Audio) {
+		setDocSize(media.file.isEmpty()
+			? media.data.size()
+			: media.filesize);
+	} else {
+		docSize = docPartSize = docPartsCount = 0;
+	}
+}
+Uploader::File::File(const std::shared_ptr<FileLoadResult> &file)
+: file(file) {
+	partsCount = (type() == SendMediaType::Photo)
+		? file->fileparts.size()
+		: file->thumbparts.size();
+	if (type() == SendMediaType::File || type() == SendMediaType::Audio) {
+		setDocSize(file->filesize);
+	} else {
+		docSize = docPartSize = docPartsCount = 0;
+	}
+}
+
+void Uploader::File::setDocSize(int32 size) {
+	docSize = size;
+	constexpr auto limit0 = 1024 * 1024;
+	constexpr auto limit1 = 32 * limit0;
+	if (docSize >= limit0 || !setPartSize(DocumentUploadPartSize0)) {
+		if (docSize > limit1 || !setPartSize(DocumentUploadPartSize1)) {
+			if (!setPartSize(DocumentUploadPartSize2)) {
+				if (!setPartSize(DocumentUploadPartSize3)) {
+					if (!setPartSize(DocumentUploadPartSize4)) {
+						LOG(("Upload Error: bad doc size: %1").arg(docSize));
+					}
+				}
+			}
+		}
+	}
+}
+
+bool Uploader::File::setPartSize(uint32 partSize) {
+	docPartSize = partSize;
+	docPartsCount = (docSize / docPartSize)
+		+ ((docSize % docPartSize) ? 1 : 0);
+	return (docPartsCount <= DocumentMaxPartsCount);
+}
+
+uint64 Uploader::File::id() const {
+	return file ? file->id : media.id;
+}
+
+SendMediaType Uploader::File::type() const {
+	return file ? file->type : media.type;
+}
+
+uint64 Uploader::File::thumbId() const {
+	return file ? file->thumbId : media.thumbId;
+}
+
+const QString &Uploader::File::filename() const {
+	return file ? file->filename : media.filename;
+}
+
 Uploader::Uploader() {
 	nextTimer.setSingleShot(true);
 	connect(&nextTimer, SIGNAL(timeout()), this, SLOT(sendNext()));
@@ -59,7 +149,9 @@ void Uploader::uploadMedia(const FullMsgId &msgId, const SendMediaReady &media)
 	sendNext();
 }
 
-void Uploader::upload(const FullMsgId &msgId, const FileLoadResultPtr &file) {
+void Uploader::upload(
+		const FullMsgId &msgId,
+		const std::shared_ptr<FileLoadResult> &file) {
 	if (file->type == SendMediaType::Photo) {
 		auto photo = App::feedPhoto(file->photo, file->photoThumbs);
 		photo->uploadingData = std::make_unique<PhotoData::UploadingData>(file->partssize);
diff --git a/Telegram/SourceFiles/storage/file_upload.h b/Telegram/SourceFiles/storage/file_upload.h
index 350529243..f8dacabfc 100644
--- a/Telegram/SourceFiles/storage/file_upload.h
+++ b/Telegram/SourceFiles/storage/file_upload.h
@@ -20,7 +20,8 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #pragma once
 
-#include "storage/localimageloader.h"
+struct FileLoadResult;
+struct SendMediaReady;
 
 namespace Storage {
 
@@ -30,7 +31,9 @@ class Uploader : public QObject, public RPCSender {
 public:
 	Uploader();
 	void uploadMedia(const FullMsgId &msgId, const SendMediaReady &image);
-	void upload(const FullMsgId &msgId, const FileLoadResultPtr &file);
+	void upload(
+		const FullMsgId &msgId,
+		const std::shared_ptr<FileLoadResult> &file);
 
 	int32 currentOffset(const FullMsgId &msgId) const; // -1 means file not found
 	int32 fullSize(const FullMsgId &msgId) const;
@@ -60,69 +63,7 @@ signals:
 	void documentFailed(const FullMsgId &msgId);
 
 private:
-	struct File {
-		File(const SendMediaReady &media) : media(media), docSentParts(0) {
-			partsCount = media.parts.size();
-			if (type() == SendMediaType::File || type() == SendMediaType::Audio) {
-				setDocSize(media.file.isEmpty() ? media.data.size() : media.filesize);
-			} else {
-				docSize = docPartSize = docPartsCount = 0;
-			}
-		}
-		File(const FileLoadResultPtr &file) : file(file), docSentParts(0) {
-			partsCount = (type() == SendMediaType::Photo) ? file->fileparts.size() : file->thumbparts.size();
-			if (type() == SendMediaType::File || type() == SendMediaType::Audio) {
-				setDocSize(file->filesize);
-			} else {
-				docSize = docPartSize = docPartsCount = 0;
-			}
-		}
-		void setDocSize(int32 size) {
-			docSize = size;
-			if (docSize >= 1024 * 1024 || !setPartSize(DocumentUploadPartSize0)) {
-				if (docSize > 32 * 1024 * 1024 || !setPartSize(DocumentUploadPartSize1)) {
-					if (!setPartSize(DocumentUploadPartSize2)) {
-						if (!setPartSize(DocumentUploadPartSize3)) {
-							if (!setPartSize(DocumentUploadPartSize4)) {
-								LOG(("Upload Error: bad doc size: %1").arg(docSize));
-							}
-						}
-					}
-				}
-			}
-		}
-		bool setPartSize(uint32 partSize) {
-			docPartSize = partSize;
-			docPartsCount = (docSize / docPartSize) + ((docSize % docPartSize) ? 1 : 0);
-			return (docPartsCount <= DocumentMaxPartsCount);
-		}
-
-		FileLoadResultPtr file;
-		SendMediaReady media;
-		int32 partsCount;
-		mutable int32 fileSentSize;
-
-		uint64 id() const {
-			return file ? file->id : media.id;
-		}
-		SendMediaType type() const {
-			return file ? file->type : media.type;
-		}
-		uint64 thumbId() const {
-			return file ? file->thumbId : media.thumbId;
-		}
-		const QString &filename() const {
-			return file ? file->filename : media.filename;
-		}
-
-		HashMd5 md5Hash;
-
-		std::unique_ptr<QFile> docFile;
-		int32 docSentParts;
-		int32 docSize;
-		int32 docPartSize;
-		int32 docPartsCount;
-	};
+	struct File;
 
 	void partLoaded(const MTPBool &result, mtpRequestId requestId);
 	bool partFailed(const RPCError &err, mtpRequestId requestId);
diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp
index 06a54a3a2..a6b272766 100644
--- a/Telegram/SourceFiles/storage/localimageloader.cpp
+++ b/Telegram/SourceFiles/storage/localimageloader.cpp
@@ -30,21 +30,16 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "lang/lang_keys.h"
 #include "boxes/confirm_box.h"
 #include "storage/file_download.h"
+#include "storage/storage_media_prepare.h"
 
-namespace {
+using Storage::ValidateThumbDimensions;
 
-bool ValidateThumbDimensions(int width, int height) {
-	return (width > 0) && (height > 0) && (width < 20 * height) && (height < 20 * width);
-}
-
-} // namespace
-
-TaskQueue::TaskQueue(QObject *parent, int32 stopTimeoutMs) : QObject(parent), _thread(0), _worker(0), _stopTimer(0) {
+TaskQueue::TaskQueue(TimeMs stopTimeoutMs) {
 	if (stopTimeoutMs > 0) {
 		_stopTimer = new QTimer(this);
 		connect(_stopTimer, SIGNAL(timeout()), this, SLOT(stop()));
 		_stopTimer->setSingleShot(true);
-		_stopTimer->setInterval(stopTimeoutMs);
+		_stopTimer->setInterval(int(stopTimeoutMs));
 	}
 }
 
@@ -189,23 +184,61 @@ void TaskQueueWorker::onTaskAdded() {
 	_inTaskAdded = false;
 }
 
-FileLoadTask::FileLoadTask(const QString &filepath, std::unique_ptr<MediaInformation> information, SendMediaType type, const FileLoadTo &to, const QString &caption) : _id(rand_value<uint64>())
+SendingAlbum::SendingAlbum() : groupId(rand_value<uint64>()) {
+}
+
+FileLoadResult::FileLoadResult(
+	TaskId taskId,
+	uint64 id,
+	const FileLoadTo &to,
+	const QString &caption,
+	std::shared_ptr<SendingAlbum> album)
+: taskId(taskId)
+, id(id)
+, to(to)
+, caption(caption)
+, album(std::move(album)) {
+}
+
+FileLoadTask::FileLoadTask(
+	const QString &filepath,
+	std::unique_ptr<FileMediaInformation> information,
+	SendMediaType type,
+	const FileLoadTo &to,
+	const QString &caption,
+	std::shared_ptr<SendingAlbum> album)
+: _id(rand_value<uint64>())
 , _to(to)
+, _album(std::move(album))
 , _filepath(filepath)
 , _information(std::move(information))
 , _type(type)
 , _caption(caption) {
 }
 
-FileLoadTask::FileLoadTask(const QByteArray &content, const QImage &image, SendMediaType type, const FileLoadTo &to, const QString &caption) : _id(rand_value<uint64>())
+FileLoadTask::FileLoadTask(
+	const QByteArray &content,
+	const QImage &image,
+	SendMediaType type,
+	const FileLoadTo &to,
+	const QString &caption,
+	std::shared_ptr<SendingAlbum> album)
+: _id(rand_value<uint64>())
 , _to(to)
+, _album(std::move(album))
 , _content(content)
 , _image(image)
 , _type(type)
 , _caption(caption) {
 }
 
-FileLoadTask::FileLoadTask(const QByteArray &voice, int32 duration, const VoiceWaveform &waveform, const FileLoadTo &to, const QString &caption) : _id(rand_value<uint64>())
+FileLoadTask::FileLoadTask(
+	const QByteArray &voice,
+	int32 duration,
+	const VoiceWaveform &waveform,
+	const FileLoadTo &to,
+	const QString &caption)
+: _id(rand_value<uint64>())
 , _to(to)
 , _content(voice)
 , _duration(duration)
@@ -214,8 +247,11 @@ FileLoadTask::FileLoadTask(const QByteArray &voice, int32 duration, const VoiceW
 , _caption(caption) {
 }
 
-std::unique_ptr<FileLoadTask::MediaInformation> FileLoadTask::ReadMediaInformation(const QString &filepath, const QByteArray &content, const QString &filemime) {
-	auto result = std::make_unique<MediaInformation>();
+std::unique_ptr<FileMediaInformation> FileLoadTask::ReadMediaInformation(
+		const QString &filepath,
+		const QByteArray &content,
+		const QString &filemime) {
+	auto result = std::make_unique<FileMediaInformation>();
 	result->filemime = filemime;
 
 	if (CheckForSong(filepath, content, result)) {
@@ -229,7 +265,11 @@ std::unique_ptr<FileLoadTask::MediaInformation> FileLoadTask::ReadMediaInformati
 }
 
 template <typename Mimes, typename Extensions>
-bool FileLoadTask::CheckMimeOrExtensions(const QString &filepath, const QString &filemime, Mimes &mimes, Extensions &extensions) {
+bool FileLoadTask::CheckMimeOrExtensions(
+		const QString &filepath,
+		const QString &filemime,
+		Mimes &mimes,
+		Extensions &extensions) {
 	if (std::find(std::begin(mimes), std::end(mimes), filemime) != std::end(mimes)) {
 		return true;
 	}
@@ -241,7 +281,10 @@ bool FileLoadTask::CheckMimeOrExtensions(const QString &filepath, const QString
 	return false;
 }
 
-bool FileLoadTask::CheckForSong(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result) {
+bool FileLoadTask::CheckForSong(
+		const QString &filepath,
+		const QByteArray &content,
+		std::unique_ptr<FileMediaInformation> &result) {
 	static const auto mimes = {
 		qstr("audio/mp3"),
 		qstr("audio/m4a"),
@@ -271,7 +314,10 @@ bool FileLoadTask::CheckForSong(const QString &filepath, const QByteArray &conte
 	return true;
 }
 
-bool FileLoadTask::CheckForVideo(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result) {
+bool FileLoadTask::CheckForVideo(
+		const QString &filepath,
+		const QByteArray &content,
+		std::unique_ptr<FileMediaInformation> &result) {
 	static const auto mimes = {
 		qstr("video/mp4"),
 		qstr("video/quicktime"),
@@ -302,7 +348,10 @@ bool FileLoadTask::CheckForVideo(const QString &filepath, const QByteArray &cont
 	return true;
 }
 
-bool FileLoadTask::CheckForImage(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result) {
+bool FileLoadTask::CheckForImage(
+		const QString &filepath,
+		const QByteArray &content,
+		std::unique_ptr<FileMediaInformation> &result) {
 	auto animated = false;
 	auto image = ([&filepath, &content, &animated] {
 		if (!content.isEmpty()) {
@@ -316,7 +365,7 @@ bool FileLoadTask::CheckForImage(const QString &filepath, const QByteArray &cont
 	if (image.isNull()) {
 		return false;
 	}
-	auto media = Image();
+	auto media = FileMediaInformation::Image();
 	media.data = std::move(image);
 	media.animated = animated;
 	result->media = media;
@@ -326,7 +375,12 @@ bool FileLoadTask::CheckForImage(const QString &filepath, const QByteArray &cont
 void FileLoadTask::process() {
 	const auto stickerMime = qsl("image/webp");
 
-	_result = std::make_shared<FileLoadResult>(_id, _to, _caption);
+	_result = std::make_shared<FileLoadResult>(
+		id(),
+		_id,
+		_to,
+		_caption,
+		_album);
 
 	QString filename, filemime;
 	qint64 filesize = 0;
@@ -360,7 +414,8 @@ void FileLoadTask::process() {
 			_information = readMediaInformation(mimeTypeForFile(info).name());
 		}
 		filemime = _information->filemime;
-		if (auto image = base::get_if<FileLoadTask::Image>(&_information->media)) {
+		if (auto image = base::get_if<FileMediaInformation::Image>(
+				&_information->media)) {
 			fullimage = base::take(image->data);
 			if (auto opaque = (filemime != stickerMime)) {
 				fullimage = Images::prepareOpaque(std::move(fullimage));
@@ -393,7 +448,8 @@ void FileLoadTask::process() {
 		}
 	} else {
 		if (_information) {
-			if (auto image = base::get_if<FileLoadTask::Image>(&_information->media)) {
+			if (auto image = base::get_if<FileMediaInformation::Image>(
+					&_information->media)) {
 				fullimage = base::take(image->data);
 			}
 		}
@@ -440,7 +496,8 @@ void FileLoadTask::process() {
 			_information = readMediaInformation(filemime);
 			filemime = _information->filemime;
 		}
-		if (auto song = base::get_if<Song>(&_information->media)) {
+		if (auto song = base::get_if<FileMediaInformation::Song>(
+				&_information->media)) {
 			isSong = true;
 			auto flags = MTPDdocumentAttributeAudio::Flag::f_title | MTPDdocumentAttributeAudio::Flag::f_performer;
 			attributes.push_back(MTP_documentAttributeAudio(MTP_flags(flags), MTP_int(song->duration), MTP_string(song->title), MTP_string(song->performer), MTPstring()));
@@ -461,7 +518,8 @@ void FileLoadTask::process() {
 
 				thumbId = rand_value<uint64>();
 			}
-		} else if (auto video = base::get_if<Video>(&_information->media)) {
+		} else if (auto video = base::get_if<FileMediaInformation::Video>(
+				&_information->media)) {
 			isVideo = true;
 			auto coverWidth = video->thumbnail.width();
 			auto coverHeight = video->thumbnail.height();
@@ -586,12 +644,27 @@ void FileLoadTask::finish() {
 		Ui::show(
 			Box<InformBox>(lng_send_image_empty(lt_name, _filepath)),
 			LayerOption::KeepOther);
+		removeFromAlbum();
 	} else if (_result->filesize > App::kFileSizeLimit) {
 		Ui::show(
 			Box<InformBox>(
 				lng_send_image_too_large(lt_name, _filepath)),
 			LayerOption::KeepOther);
+		removeFromAlbum();
 	} else if (App::main()) {
 		App::main()->onSendFileConfirm(_result);
 	}
 }
+
+void FileLoadTask::removeFromAlbum() {
+	if (!_album) {
+		return;
+	}
+	const auto proj = [](const SendingAlbum::Item &item) {
+		return item.taskId;
+	};
+	const auto it = ranges::find(_album->items, id(), proj);
+	Assert(it != _album->items.end());
+
+	_album->items.erase(it);
+}
diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h
index d5a3a8820..7d864b761 100644
--- a/Telegram/SourceFiles/storage/localimageloader.h
+++ b/Telegram/SourceFiles/storage/localimageloader.h
@@ -120,7 +120,7 @@ class TaskQueue : public QObject {
 	Q_OBJECT
 
 public:
-	TaskQueue(QObject *parent, int32 stopTimeoutMs = 0); // <= 0 - never stop worker
+	explicit TaskQueue(TimeMs stopTimeoutMs = 0); // <= 0 - never stop worker
 
 	TaskId addTask(std::unique_ptr<Task> &&task);
 	void addTasks(std::vector<std::unique_ptr<Task>> &&tasks);
@@ -144,9 +144,9 @@ private:
 	std::deque<std::unique_ptr<Task>> _tasksToFinish;
 	TaskId _taskInProcessId = TaskId();
 	QMutex _tasksToProcessMutex, _tasksToFinishMutex;
-	QThread *_thread;
-	TaskQueueWorker *_worker;
-	QTimer *_stopTimer;
+	QThread *_thread = nullptr;
+	TaskQueueWorker *_worker = nullptr;
+	QTimer *_stopTimer = nullptr;
 
 };
 
@@ -169,6 +169,22 @@ private:
 
 };
 
+struct SendingAlbum {
+	struct Item {
+		explicit Item(TaskId taskId) : taskId(taskId) {
+		}
+		TaskId taskId;
+		FullMsgId msgId;
+		base::optional<MTPInputSingleMedia> media;
+	};
+
+	SendingAlbum();
+
+	uint64 groupId;
+	std::vector<Item> items;
+
+};
+
 struct FileLoadTo {
 	FileLoadTo(const PeerId &peer, bool silent, MsgId replyTo)
 		: peer(peer)
@@ -181,14 +197,17 @@ struct FileLoadTo {
 };
 
 struct FileLoadResult {
-	FileLoadResult(const uint64 &id, const FileLoadTo &to, const QString &caption)
-		: id(id)
-		, to(to)
-		, caption(caption) {
-	}
+	FileLoadResult(
+		TaskId taskId,
+		uint64 id,
+		const FileLoadTo &to,
+		const QString &caption,
+		std::shared_ptr<SendingAlbum> album);
 
+	TaskId taskId;
 	uint64 id;
 	FileLoadTo to;
+	std::shared_ptr<SendingAlbum> album;
 	SendMediaType type = SendMediaType::File;
 	QString filepath;
 	QByteArray content;
@@ -235,10 +254,8 @@ struct FileLoadResult {
 		}
 	}
 };
-using FileLoadResultPtr = std::shared_ptr<FileLoadResult>;
 
-class FileLoadTask final : public Task {
-public:
+struct FileMediaInformation {
 	struct Image {
 		QImage data;
 		bool animated = false;
@@ -254,15 +271,38 @@ public:
 		int duration = -1;
 		QImage thumbnail;
 	};
-	struct MediaInformation {
-		QString filemime;
-		base::variant<Image, Song, Video> media;
-	};
-	static std::unique_ptr<MediaInformation> ReadMediaInformation(const QString &filepath, const QByteArray &content, const QString &filemime);
 
-	FileLoadTask(const QString &filepath, std::unique_ptr<MediaInformation> information, SendMediaType type, const FileLoadTo &to, const QString &caption);
-	FileLoadTask(const QByteArray &content, const QImage &image, SendMediaType type, const FileLoadTo &to, const QString &caption);
-	FileLoadTask(const QByteArray &voice, int32 duration, const VoiceWaveform &waveform, const FileLoadTo &to, const QString &caption);
+	QString filemime;
+	base::variant<Image, Song, Video> media;
+};
+
+class FileLoadTask final : public Task {
+public:
+	static std::unique_ptr<FileMediaInformation> ReadMediaInformation(
+		const QString &filepath,
+		const QByteArray &content,
+		const QString &filemime);
+
+	FileLoadTask(
+		const QString &filepath,
+		std::unique_ptr<FileMediaInformation> information,
+		SendMediaType type,
+		const FileLoadTo &to,
+		const QString &caption,
+		std::shared_ptr<SendingAlbum> album = nullptr);
+	FileLoadTask(
+		const QByteArray &content,
+		const QImage &image,
+		SendMediaType type,
+		const FileLoadTo &to,
+		const QString &caption,
+		std::shared_ptr<SendingAlbum> album = nullptr);
+	FileLoadTask(
+		const QByteArray &voice,
+		int32 duration,
+		const VoiceWaveform &waveform,
+		const FileLoadTo &to,
+		const QString &caption);
 
 	uint64 fileid() const {
 		return _id;
@@ -272,28 +312,30 @@ public:
 	void finish();
 
 private:
-	static bool CheckForSong(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result);
-	static bool CheckForVideo(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result);
-	static bool CheckForImage(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result);
+	static bool CheckForSong(const QString &filepath, const QByteArray &content, std::unique_ptr<FileMediaInformation> &result);
+	static bool CheckForVideo(const QString &filepath, const QByteArray &content, std::unique_ptr<FileMediaInformation> &result);
+	static bool CheckForImage(const QString &filepath, const QByteArray &content, std::unique_ptr<FileMediaInformation> &result);
 
 	template <typename Mimes, typename Extensions>
 	static bool CheckMimeOrExtensions(const QString &filepath, const QString &filemime, Mimes &mimes, Extensions &extensions);
 
-	std::unique_ptr<MediaInformation> readMediaInformation(const QString &filemime) const {
+	std::unique_ptr<FileMediaInformation> readMediaInformation(const QString &filemime) const {
 		return ReadMediaInformation(_filepath, _content, filemime);
 	}
+	void removeFromAlbum();
 
 	uint64 _id;
 	FileLoadTo _to;
+	const std::shared_ptr<SendingAlbum> _album;
 	QString _filepath;
 	QByteArray _content;
-	std::unique_ptr<MediaInformation> _information;
+	std::unique_ptr<FileMediaInformation> _information;
 	QImage _image;
 	int32 _duration = 0;
 	VoiceWaveform _waveform;
 	SendMediaType _type;
 	QString _caption;
 
-	FileLoadResultPtr _result;
+	std::shared_ptr<FileLoadResult> _result;
 
 };
diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp
index 5e0ac58e0..68240fcb3 100644
--- a/Telegram/SourceFiles/storage/localstorage.cpp
+++ b/Telegram/SourceFiles/storage/localstorage.cpp
@@ -44,12 +44,13 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 namespace Local {
 namespace {
 
-constexpr int kThemeFileSizeLimit = 5 * 1024 * 1024;
+constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;
+constexpr auto kFileLoaderQueueStopTimeout = TimeMs(5000);
 
 using FileKey = quint64;
 
 constexpr char tdfMagic[] = { 'T', 'D', 'F', '$' };
-constexpr int tdfMagicLen = sizeof(tdfMagic);
+constexpr auto tdfMagicLen = int(sizeof(tdfMagic));
 
 QString toFilePart(FileKey val) {
 	QString result;
@@ -2273,7 +2274,7 @@ void start() {
 	Expects(!_manager);
 
 	_manager = new internal::Manager();
-	_localLoader = new TaskQueue(0, FileLoaderQueueStopTimeout);
+	_localLoader = new TaskQueue(kFileLoaderQueueStopTimeout);
 
 	_basePath = cWorkingDir() + qsl("tdata/");
 	if (!QDir().exists(_basePath)) QDir().mkpath(_basePath);
diff --git a/Telegram/SourceFiles/storage/storage_media_prepare.cpp b/Telegram/SourceFiles/storage/storage_media_prepare.cpp
new file mode 100644
index 000000000..968178449
--- /dev/null
+++ b/Telegram/SourceFiles/storage/storage_media_prepare.cpp
@@ -0,0 +1,236 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop version of Telegram messaging app, see https://telegram.org
+
+Telegram Desktop is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+It is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+In addition, as a special exception, the copyright holders give permission
+to link the code of portions of this program with the OpenSSL library.
+
+Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
+Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
+*/
+#include "storage/storage_media_prepare.h"
+
+#include "platform/platform_file_utilities.h"
+#include "base/task_queue.h"
+#include "storage/localimageloader.h"
+
+namespace Storage {
+namespace {
+
+constexpr auto kMaxAlbumCount = 10;
+
+bool HasExtensionFrom(const QString &file, const QStringList &extensions) {
+	for (const auto &extension : extensions) {
+		const auto ext = file.right(extension.size());
+		if (ext.compare(extension, Qt::CaseInsensitive) == 0) {
+			return true;
+		}
+	}
+	return false;
+}
+
+bool ValidPhotoForAlbum(const FileMediaInformation::Image &image) {
+	if (image.animated) {
+		return false;
+	}
+	const auto width = image.data.width();
+	const auto height = image.data.height();
+	return ValidateThumbDimensions(width, height);
+}
+
+bool ValidVideoForAlbum(const FileMediaInformation::Video &video) {
+	const auto width = video.thumbnail.width();
+	const auto height = video.thumbnail.height();
+	return ValidateThumbDimensions(width, height);
+}
+
+bool PrepareAlbumMediaIsWaiting(
+		QSemaphore &semaphore,
+		PreparedFile &file,
+		int previewWidth) {
+	// Use some special thread queue, like a separate QThreadPool.
+	base::TaskQueue::Normal().Put([&, previewWidth] {
+		const auto guard = gsl::finally([&] { semaphore.release(); });
+		const auto filemime = mimeTypeForFile(QFileInfo(file.path)).name();
+		file.information = FileLoadTask::ReadMediaInformation(
+			file.path,
+			QByteArray(),
+			filemime);
+		using Image = FileMediaInformation::Image;
+		using Video = FileMediaInformation::Video;
+		if (const auto image = base::get_if<Image>(
+				&file.information->media)) {
+			if (ValidPhotoForAlbum(*image)) {
+				file.preview = image->data.scaledToWidth(
+					previewWidth * cIntRetinaFactor(),
+					Qt::SmoothTransformation);
+				file.preview.setDevicePixelRatio(cRetinaFactor());
+				file.type = PreparedFile::AlbumType::Photo;
+			}
+		} else if (const auto video = base::get_if<Video>(
+				&file.information->media)) {
+			if (ValidVideoForAlbum(*video)) {
+				auto blurred = Images::prepareBlur(video->thumbnail);
+				file.preview = std::move(blurred).scaledToWidth(
+					previewWidth * cIntRetinaFactor(),
+					Qt::SmoothTransformation);
+				file.preview.setDevicePixelRatio(cRetinaFactor());
+				file.type = PreparedFile::AlbumType::Video;
+			}
+		}
+	});
+	return true;
+}
+
+void PrepareAlbum(PreparedList &result, int previewWidth) {
+	const auto count = int(result.files.size());
+	if ((count < 2) || (count > kMaxAlbumCount)) {
+		return;
+	}
+
+	result.albumIsPossible = true;
+	auto waiting = 0;
+	QSemaphore semaphore;
+	for (auto &file : result.files) {
+		if (!result.albumIsPossible) {
+			break;
+		}
+		if (PrepareAlbumMediaIsWaiting(semaphore, file, previewWidth)) {
+			++waiting;
+		}
+	}
+	if (waiting > 0) {
+		semaphore.acquire(waiting);
+	}
+}
+
+} // namespace
+
+bool ValidateThumbDimensions(int width, int height) {
+	return (width > 0)
+		&& (height > 0)
+		&& (width < 20 * height)
+		&& (height < 20 * width);
+}
+
+PreparedFile::PreparedFile(const QString &path) : path(path) {
+}
+
+PreparedFile::PreparedFile(PreparedFile &&other) = default;
+
+PreparedFile &PreparedFile::operator=(PreparedFile &&other) = default;
+
+PreparedFile::~PreparedFile() = default;
+
+MimeDataState ComputeMimeDataState(const QMimeData *data) {
+	if (!data
+		|| data->hasFormat(qsl("application/x-td-forward-selected"))
+		|| data->hasFormat(qsl("application/x-td-forward-pressed"))
+		|| data->hasFormat(qsl("application/x-td-forward-pressed-link"))) {
+		return MimeDataState::None;
+	}
+
+	if (data->hasImage()) {
+		return MimeDataState::Image;
+	}
+
+	const auto uriListFormat = qsl("text/uri-list");
+	if (!data->hasFormat(uriListFormat)) {
+		return MimeDataState::None;
+	}
+
+	const auto &urls = data->urls();
+	if (urls.isEmpty()) {
+		return MimeDataState::None;
+	}
+
+	const auto imageExtensions = cImgExtensions();
+	auto files = QStringList();
+	auto allAreSmallImages = true;
+	for (const auto &url : urls) {
+		if (!url.isLocalFile()) {
+			return MimeDataState::None;
+		}
+		const auto file = Platform::File::UrlToLocal(url);
+
+		const auto info = QFileInfo(file);
+		if (info.isDir()) {
+			return MimeDataState::None;
+		}
+
+		const auto filesize = info.size();
+		if (filesize > App::kFileSizeLimit) {
+			return MimeDataState::None;
+		} else if (allAreSmallImages) {
+			if (filesize > App::kImageSizeLimit) {
+				allAreSmallImages = false;
+			} else if (!HasExtensionFrom(file, imageExtensions)) {
+				allAreSmallImages = false;
+			}
+		}
+	}
+	return allAreSmallImages
+		? MimeDataState::PhotoFiles
+		: MimeDataState::Files;
+}
+
+PreparedList PrepareMediaList(const QList<QUrl> &files, int previewWidth) {
+	auto locals = QStringList();
+	locals.reserve(files.size());
+	for (const auto &url : files) {
+		if (!url.isLocalFile()) {
+			return {
+				PreparedList::Error::NonLocalUrl,
+				url.toDisplayString()
+			};
+		}
+		locals.push_back(Platform::File::UrlToLocal(url));
+	}
+	return PrepareMediaList(locals, previewWidth);
+}
+
+PreparedList PrepareMediaList(const QStringList &files, int previewWidth) {
+	auto result = PreparedList();
+	result.files.reserve(files.size());
+	const auto extensionsToCompress = cExtensionsForCompress();
+	for (const auto &file : files) {
+		const auto fileinfo = QFileInfo(file);
+		const auto filesize = fileinfo.size();
+		if (fileinfo.isDir()) {
+			return {
+				PreparedList::Error::Directory,
+				file
+			};
+		} else if (filesize <= 0) {
+			return {
+				PreparedList::Error::EmptyFile,
+				file
+			};
+		} else if (filesize > App::kFileSizeLimit) {
+			return {
+				PreparedList::Error::TooLargeFile,
+				file
+			};
+		}
+		const auto toCompress = HasExtensionFrom(file, extensionsToCompress);
+		if (filesize > App::kImageSizeLimit || !toCompress) {
+			result.allFilesForCompress = false;
+		}
+		result.files.push_back({ file });
+	}
+	PrepareAlbum(result, previewWidth);
+	return result;
+}
+
+} // namespace Storage
+
diff --git a/Telegram/SourceFiles/storage/storage_media_prepare.h b/Telegram/SourceFiles/storage/storage_media_prepare.h
new file mode 100644
index 000000000..5b7887f43
--- /dev/null
+++ b/Telegram/SourceFiles/storage/storage_media_prepare.h
@@ -0,0 +1,83 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop version of Telegram messaging app, see https://telegram.org
+
+Telegram Desktop is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+It is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+In addition, as a special exception, the copyright holders give permission
+to link the code of portions of this program with the OpenSSL library.
+
+Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
+Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
+*/
+#pragma once
+
+struct FileMediaInformation;
+
+namespace Storage {
+
+enum class MimeDataState {
+	None,
+	Files,
+	PhotoFiles,
+	Image,
+};
+
+MimeDataState ComputeMimeDataState(const QMimeData *data);
+
+struct PreparedFile {
+	enum class AlbumType {
+		None,
+		Photo,
+		Video,
+	};
+
+	PreparedFile(const QString &path);
+	PreparedFile(PreparedFile &&other);
+	PreparedFile &operator=(PreparedFile &&other);
+	~PreparedFile();
+
+	QString path;
+	std::unique_ptr<FileMediaInformation> information;
+	base::optional<QImage> large;
+	QImage preview;
+	AlbumType type = AlbumType::None;
+
+};
+
+struct PreparedList {
+	enum class Error {
+		None,
+		NonLocalUrl,
+		Directory,
+		EmptyFile,
+		TooLargeFile,
+	};
+
+	PreparedList() = default;
+	PreparedList(Error error, QString errorData)
+	: error(error)
+	, errorData(errorData) {
+	}
+
+	Error error = Error::None;
+	QString errorData;
+	std::vector<PreparedFile> files;
+	bool allFilesForCompress = true;
+	bool albumIsPossible = false;
+
+};
+
+bool ValidateThumbDimensions(int width, int height);
+PreparedList PrepareMediaList(const QList<QUrl> &files, int previewWidth);
+PreparedList PrepareMediaList(const QStringList &files, int previewWidth);
+
+} // namespace Storage
diff --git a/Telegram/SourceFiles/ui/grouped_layout.cpp b/Telegram/SourceFiles/ui/grouped_layout.cpp
index 0a14dbb60..1bc06227e 100644
--- a/Telegram/SourceFiles/ui/grouped_layout.cpp
+++ b/Telegram/SourceFiles/ui/grouped_layout.cpp
@@ -20,7 +20,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #include "ui/grouped_layout.h"
 
-namespace Data {
+namespace Ui {
 namespace {
 
 int Round(float64 value) {
@@ -586,4 +586,47 @@ std::vector<GroupMediaLayout> LayoutMediaGroup(
 	return Layouter(sizes, maxWidth, minWidth, spacing).layout();
 }
 
-} // namespace Data
+RectParts GetCornersFromSides(RectParts sides) {
+	const auto convert = [&](
+			RectPart side1,
+			RectPart side2,
+			RectPart corner) {
+		return ((sides & side1) && (sides & side2))
+			? corner
+			: RectPart::None;
+	};
+	return RectPart::None
+		| convert(RectPart::Top, RectPart::Left, RectPart::TopLeft)
+		| convert(RectPart::Top, RectPart::Right, RectPart::TopRight)
+		| convert(RectPart::Bottom, RectPart::Left, RectPart::BottomLeft)
+		| convert(RectPart::Bottom, RectPart::Right, RectPart::BottomRight);
+}
+
+QSize GetImageScaleSizeForGeometry(QSize original, QSize geometry) {
+	const auto width = geometry.width();
+	const auto height = geometry.height();
+	auto tw = original.width();
+	auto th = original.height();
+	if (tw * height > th * width) {
+		if (th > height || tw * height < 2 * th * width) {
+			tw = (height * tw) / th;
+			th = height;
+		} else if (tw < width) {
+			th = (width * th) / tw;
+			tw = width;
+		}
+	} else {
+		if (tw > width || th * width < 2 * tw * height) {
+			th = (width * th) / tw;
+			tw = width;
+		} else if (tw > 0 && th < height) {
+			tw = (height * tw) / th;
+			th = height;
+		}
+	}
+	if (tw < 1) tw = 1;
+	if (th < 1) th = 1;
+	return { tw, th };
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/grouped_layout.h b/Telegram/SourceFiles/ui/grouped_layout.h
index 1efb4b9cb..3a02f34d8 100644
--- a/Telegram/SourceFiles/ui/grouped_layout.h
+++ b/Telegram/SourceFiles/ui/grouped_layout.h
@@ -20,7 +20,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 */
 #pragma once
 
-namespace Data {
+namespace Ui {
 
 struct GroupMediaLayout {
 	QRect geometry;
@@ -33,4 +33,7 @@ std::vector<GroupMediaLayout> LayoutMediaGroup(
 	int minWidth,
 	int spacing);
 
-} // namespace Data
+RectParts GetCornersFromSides(RectParts sides);
+QSize GetImageScaleSizeForGeometry(QSize original, QSize geometry);
+
+} // namespace Ui
diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt
index 719c1a366..ee7176f04 100644
--- a/Telegram/gyp/telegram_sources.txt
+++ b/Telegram/gyp/telegram_sources.txt
@@ -519,6 +519,8 @@
 <(src_loc)/storage/serialize_document.h
 <(src_loc)/storage/storage_facade.cpp
 <(src_loc)/storage/storage_facade.h
+<(src_loc)/storage/storage_media_prepare.cpp
+<(src_loc)/storage/storage_media_prepare.h
 <(src_loc)/storage/storage_shared_media.cpp
 <(src_loc)/storage/storage_shared_media.h
 <(src_loc)/storage/storage_sparse_ids_list.cpp