1185 lines
		
	
	
	
		
			33 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1185 lines
		
	
	
	
		
			33 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| This file is part of Telegram Desktop,
 | |
| the official desktop application for the Telegram messaging service.
 | |
| 
 | |
| For license and copyright information please follow this link:
 | |
| https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | |
| */
 | |
| #include "data/data_download_manager.h"
 | |
| 
 | |
| #include "data/data_session.h"
 | |
| #include "data/data_photo.h"
 | |
| #include "data/data_document.h"
 | |
| #include "data/data_document_media.h"
 | |
| #include "data/data_web_page.h"
 | |
| #include "data/data_changes.h"
 | |
| #include "data/data_user.h"
 | |
| #include "data/data_channel.h"
 | |
| #include "data/data_file_origin.h"
 | |
| #include "base/unixtime.h"
 | |
| #include "base/random.h"
 | |
| #include "main/main_session.h"
 | |
| #include "main/main_account.h"
 | |
| #include "storage/storage_account.h"
 | |
| #include "history/history.h"
 | |
| #include "history/history_item.h"
 | |
| #include "history/history_message.h"
 | |
| #include "core/application.h"
 | |
| #include "core/mime_type.h"
 | |
| #include "ui/controls/download_bar.h"
 | |
| #include "ui/text/format_song_document_name.h"
 | |
| #include "ui/layers/generic_box.h"
 | |
| #include "storage/serialize_common.h"
 | |
| #include "window/window_controller.h"
 | |
| #include "window/window_session_controller.h"
 | |
| #include "apiwrap.h"
 | |
| #include "styles/style_layers.h"
 | |
| 
 | |
| namespace Data {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kClearLoadingTimeout = 5 * crl::time(1000);
 | |
| constexpr auto kMaxFileSize = 4000 * int64(1024 * 1024);
 | |
| constexpr auto kMaxResolvePerAttempt = 100;
 | |
| 
 | |
| constexpr auto ByItem = [](const auto &entry) {
 | |
| 	if constexpr (std::is_same_v<decltype(entry), const DownloadingId&>) {
 | |
| 		return entry.object.item;
 | |
| 	} else {
 | |
| 		const auto resolved = entry.object.get();
 | |
| 		return resolved ? resolved->item.get() : nullptr;
 | |
| 	}
 | |
| };
 | |
| 
 | |
| constexpr auto ByDocument = [](const auto &entry) {
 | |
| 	return entry.object.document;
 | |
| };
 | |
| 
 | |
| [[nodiscard]] uint64 PeerAccessHash(not_null<PeerData*> peer) {
 | |
| 	if (const auto user = peer->asUser()) {
 | |
| 		return user->accessHash();
 | |
| 	} else if (const auto channel = peer->asChannel()) {
 | |
| 		return channel->access;
 | |
| 	}
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] PhotoData *ItemPhoto(not_null<HistoryItem*> item) {
 | |
| 	if (const auto media = item->media()) {
 | |
| 		if (const auto page = media->webpage()) {
 | |
| 			return page->document ? nullptr : page->photo;
 | |
| 		} else if (const auto photo = media->photo()) {
 | |
| 			return photo;
 | |
| 		}
 | |
| 	}
 | |
| 	return nullptr;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] DocumentData *ItemDocument(not_null<HistoryItem*> item) {
 | |
| 	if (const auto media = item->media()) {
 | |
| 		if (const auto page = media->webpage()) {
 | |
| 			return page->document;
 | |
| 		} else if (const auto document = media->document()) {
 | |
| 			return document;
 | |
| 		}
 | |
| 	}
 | |
| 	return nullptr;
 | |
| }
 | |
| 
 | |
| struct DocumentDescriptor {
 | |
| 	uint64 sessionUniqueId = 0;
 | |
| 	DocumentId documentId = 0;
 | |
| 	FullMsgId itemId;
 | |
| };
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| struct DownloadManager::DeleteFilesDescriptor {
 | |
| 	base::flat_set<not_null<Main::Session*>> sessions;
 | |
| 	base::flat_map<QString, DocumentDescriptor> files;
 | |
| };
 | |
| 
 | |
| DownloadManager::DownloadManager()
 | |
| : _clearLoadingTimer([=] { clearLoading(); }) {
 | |
| }
 | |
| 
 | |
| DownloadManager::~DownloadManager() = default;
 | |
| 
 | |
| void DownloadManager::trackSession(not_null<Main::Session*> session) {
 | |
| 	auto &data = _sessions.emplace(session, SessionData()).first->second;
 | |
| 	data.downloaded = deserialize(session);
 | |
| 	data.resolveNeeded = data.downloaded.size();
 | |
| 
 | |
| 	session->data().documentLoadProgress(
 | |
| 	) | rpl::filter([=](not_null<DocumentData*> document) {
 | |
| 		return _loadingDocuments.contains(document);
 | |
| 	}) | rpl::start_with_next([=](not_null<DocumentData*> document) {
 | |
| 		check(document);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->data().itemLayoutChanged(
 | |
| 	) | rpl::filter([=](not_null<const HistoryItem*> item) {
 | |
| 		return _loading.contains(item);
 | |
| 	}) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
 | |
| 		check(item);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->data().itemViewRefreshRequest(
 | |
| 	) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
 | |
| 		changed(item);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->changes().messageUpdates(
 | |
| 		MessageUpdate::Flag::Destroyed
 | |
| 	) | rpl::start_with_next([=](const MessageUpdate &update) {
 | |
| 		removed(update.item);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->account().sessionChanges(
 | |
| 	) | rpl::filter(
 | |
| 		rpl::mappers::_1 != session
 | |
| 	) | rpl::take(1) | rpl::start_with_next([=] {
 | |
| 		untrack(session);
 | |
| 	}, data.lifetime);
 | |
| }
 | |
| 
 | |
| void DownloadManager::itemVisibilitiesUpdated(
 | |
| 		not_null<Main::Session*> session) {
 | |
| 	const auto i = _sessions.find(session);
 | |
| 	if (i == end(_sessions)
 | |
| 		|| i->second.downloading.empty()
 | |
| 		|| !i->second.downloading.front().hiddenByView) {
 | |
| 		return;
 | |
| 	}
 | |
| 	for (const auto &id : i->second.downloading) {
 | |
| 		if (!id.done
 | |
| 			&& !session->data().queryItemVisibility(id.object.item)) {
 | |
| 			for (auto &id : i->second.downloading) {
 | |
| 				id.hiddenByView = false;
 | |
| 			}
 | |
| 			_loadingListChanges.fire({});
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int64 DownloadManager::computeNextStartDate() {
 | |
| 	const auto now = base::unixtime::now();
 | |
| 	if (_lastStartedBase != now) {
 | |
| 		_lastStartedBase = now;
 | |
| 		_lastStartedAdded = 0;
 | |
| 	} else {
 | |
| 		++_lastStartedAdded;
 | |
| 	}
 | |
| 	return int64(_lastStartedBase) * 1000 + _lastStartedAdded;
 | |
| }
 | |
| 
 | |
| void DownloadManager::addLoading(DownloadObject object) {
 | |
| 	Expects(object.item != nullptr);
 | |
| 	Expects(object.document != nullptr);
 | |
| 
 | |
| 	const auto item = object.item;
 | |
| 	auto &data = sessionData(item);
 | |
| 
 | |
| 	const auto already = ranges::find(data.downloading, item, ByItem);
 | |
| 	if (already != end(data.downloading)) {
 | |
| 		const auto document = already->object.document;
 | |
| 		const auto photo = already->object.photo;
 | |
| 		if (document == object.document && photo == object.photo) {
 | |
| 			check(item);
 | |
| 			return;
 | |
| 		}
 | |
| 		remove(data, already);
 | |
| 	}
 | |
| 
 | |
| 	const auto size = object.document->size;
 | |
| 	const auto path = object.document->loadingFilePath();
 | |
| 	if (path.isEmpty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto shownExists = !data.downloading.empty()
 | |
| 		&& !data.downloading.front().hiddenByView;
 | |
| 	data.downloading.push_back({
 | |
| 		.object = object,
 | |
| 		.started = computeNextStartDate(),
 | |
| 		.path = path,
 | |
| 		.total = size,
 | |
| 		.hiddenByView = (!shownExists
 | |
| 			&& item->history()->owner().queryItemVisibility(item)),
 | |
| 	});
 | |
| 	_loading.emplace(item);
 | |
| 	_loadingDocuments.emplace(object.document);
 | |
| 	_loadingProgress = DownloadProgress{
 | |
| 		.ready = _loadingProgress.current().ready,
 | |
| 		.total = _loadingProgress.current().total + size,
 | |
| 	};
 | |
| 	_loadingListChanges.fire({});
 | |
| 	_clearLoadingTimer.cancel();
 | |
| 
 | |
| 	check(item);
 | |
| }
 | |
| 
 | |
| void DownloadManager::check(not_null<const HistoryItem*> item) {
 | |
| 	auto &data = sessionData(item);
 | |
| 	const auto i = ranges::find(data.downloading, item, ByItem);
 | |
| 	Assert(i != end(data.downloading));
 | |
| 	check(data, i);
 | |
| }
 | |
| 
 | |
| void DownloadManager::check(not_null<DocumentData*> document) {
 | |
| 	auto &data = sessionData(document);
 | |
| 	const auto i = ranges::find(
 | |
| 		data.downloading,
 | |
| 		document.get(),
 | |
| 		ByDocument);
 | |
| 	Assert(i != end(data.downloading));
 | |
| 	check(data, i);
 | |
| }
 | |
| 
 | |
| void DownloadManager::check(
 | |
| 		SessionData &data,
 | |
| 		std::vector<DownloadingId>::iterator i) {
 | |
| 	auto &entry = *i;
 | |
| 
 | |
| 	const auto photo = ItemPhoto(entry.object.item);
 | |
| 	const auto document = ItemDocument(entry.object.item);
 | |
| 	if (entry.object.photo != photo || entry.object.document != document) {
 | |
| 		cancel(data, i);
 | |
| 		return;
 | |
| 	}
 | |
| 	// Load with progress only documents for now.
 | |
| 	Assert(document != nullptr);
 | |
| 
 | |
| 	const auto path = document->filepath(true);
 | |
| 	if (!path.isEmpty()) {
 | |
| 		if (_loading.contains(entry.object.item)) {
 | |
| 			addLoaded(entry.object, path, entry.started);
 | |
| 		}
 | |
| 	} else if (!document->loading()) {
 | |
| 		remove(data, i);
 | |
| 	} else {
 | |
| 		const auto totalChange = document->size - entry.total;
 | |
| 		const auto readyChange = document->loadOffset() - entry.ready;
 | |
| 		if (!readyChange && !totalChange) {
 | |
| 			return;
 | |
| 		}
 | |
| 		entry.ready += readyChange;
 | |
| 		entry.total += totalChange;
 | |
| 		_loadingProgress = DownloadProgress{
 | |
| 			.ready = _loadingProgress.current().ready + readyChange,
 | |
| 			.total = _loadingProgress.current().total + totalChange,
 | |
| 		};
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::addLoaded(
 | |
| 		DownloadObject object,
 | |
| 		const QString &path,
 | |
| 		DownloadDate started) {
 | |
| 	Expects(object.item != nullptr);
 | |
| 	Expects(object.document || object.photo);
 | |
| 
 | |
| 	const auto size = QFileInfo(path).size();
 | |
| 	if (size <= 0 || size > kMaxFileSize) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto item = object.item;
 | |
| 	auto &data = sessionData(item);
 | |
| 
 | |
| 	const auto id = object.document
 | |
| 		? DownloadId{ object.document->id, DownloadType::Document }
 | |
| 		: DownloadId{ object.photo->id, DownloadType::Photo };
 | |
| 	data.downloaded.push_back({
 | |
| 		.download = id,
 | |
| 		.started = started,
 | |
| 		.path = path,
 | |
| 		.size = size,
 | |
| 		.itemId = item->fullId(),
 | |
| 		.peerAccessHash = PeerAccessHash(item->history()->peer),
 | |
| 		.object = std::make_unique<DownloadObject>(object),
 | |
| 	});
 | |
| 	_loaded.emplace(item);
 | |
| 	_loadedAdded.fire(&data.downloaded.back());
 | |
| 
 | |
| 	writePostponed(&item->history()->session());
 | |
| 
 | |
| 	const auto i = ranges::find(data.downloading, item, ByItem);
 | |
| 	if (i != end(data.downloading)) {
 | |
| 		auto &entry = *i;
 | |
| 		const auto document = entry.object.document;
 | |
| 		if (document) {
 | |
| 			_loadingDocuments.remove(document);
 | |
| 		}
 | |
| 		const auto j = _loading.find(entry.object.item);
 | |
| 		if (j == end(_loading)) {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto totalChange = document->size - entry.total;
 | |
| 		const auto readyChange = document->size - entry.ready;
 | |
| 		entry.ready += readyChange;
 | |
| 		entry.total += totalChange;
 | |
| 		entry.done = true;
 | |
| 		_loading.erase(j);
 | |
| 		_loadingDone.emplace(entry.object.item);
 | |
| 		_loadingProgress = DownloadProgress{
 | |
| 			.ready = _loadingProgress.current().ready + readyChange,
 | |
| 			.total = _loadingProgress.current().total + totalChange,
 | |
| 		};
 | |
| 		_loadingListChanges.fire({});
 | |
| 		if (_loading.empty()) {
 | |
| 			_clearLoadingTimer.callOnce(kClearLoadingTimeout);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::clearIfFinished() {
 | |
| 	if (_clearLoadingTimer.isActive()) {
 | |
| 		_clearLoadingTimer.cancel();
 | |
| 		clearLoading();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::deleteFiles(const std::vector<GlobalMsgId> &ids) {
 | |
| 	auto descriptor = DeleteFilesDescriptor();
 | |
| 	for (const auto &id : ids) {
 | |
| 		if (const auto item = MessageByGlobalId(id)) {
 | |
| 			const auto session = &item->history()->session();
 | |
| 			const auto i = _sessions.find(session);
 | |
| 			if (i == end(_sessions)) {
 | |
| 				continue;
 | |
| 			}
 | |
| 			auto &data = i->second;
 | |
| 			const auto j = ranges::find(
 | |
| 				data.downloading,
 | |
| 				not_null{ item },
 | |
| 				ByItem);
 | |
| 			if (j != end(data.downloading)) {
 | |
| 				cancel(data, j);
 | |
| 			}
 | |
| 
 | |
| 			const auto k = ranges::find(data.downloaded, item, ByItem);
 | |
| 			if (k != end(data.downloaded)) {
 | |
| 				const auto document = k->object->document;
 | |
| 				descriptor.files.emplace(k->path, DocumentDescriptor{
 | |
| 					.sessionUniqueId = id.sessionUniqueId,
 | |
| 					.documentId = document ? document->id : DocumentId(),
 | |
| 					.itemId = id.itemId,
 | |
| 				});
 | |
| 				_loaded.remove(item);
 | |
| 				_generated.remove(item);
 | |
| 				if (document) {
 | |
| 					_generatedDocuments.remove(document);
 | |
| 				}
 | |
| 				data.downloaded.erase(k);
 | |
| 				_loadedRemoved.fire_copy(item);
 | |
| 
 | |
| 				descriptor.sessions.emplace(session);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	finishFilesDelete(std::move(descriptor));
 | |
| }
 | |
| 
 | |
| void DownloadManager::deleteAll() {
 | |
| 	auto descriptor = DeleteFilesDescriptor();
 | |
| 	for (auto &[session, data] : _sessions) {
 | |
| 		if (!data.downloaded.empty()) {
 | |
| 			descriptor.sessions.emplace(session);
 | |
| 		} else if (data.downloading.empty()) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		const auto sessionUniqueId = session->uniqueId();
 | |
| 		while (!data.downloading.empty()) {
 | |
| 			cancel(data, data.downloading.end() - 1);
 | |
| 		}
 | |
| 		for (auto &id : base::take(data.downloaded)) {
 | |
| 			const auto object = id.object.get();
 | |
| 			const auto document = object ? object->document : nullptr;
 | |
| 			descriptor.files.emplace(id.path, DocumentDescriptor{
 | |
| 				.sessionUniqueId = sessionUniqueId,
 | |
| 				.documentId = document ? document->id : DocumentId(),
 | |
| 				.itemId = id.itemId,
 | |
| 			});
 | |
| 			if (document) {
 | |
| 				_generatedDocuments.remove(document);
 | |
| 			}
 | |
| 			if (const auto item = object ? object->item.get() : nullptr) {
 | |
| 				_loaded.remove(item);
 | |
| 				_generated.remove(item);
 | |
| 				_loadedRemoved.fire_copy(item);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	for (const auto &session : descriptor.sessions) {
 | |
| 		writePostponed(session);
 | |
| 	}
 | |
| 	finishFilesDelete(std::move(descriptor));
 | |
| }
 | |
| 
 | |
| void DownloadManager::finishFilesDelete(DeleteFilesDescriptor &&descriptor) {
 | |
| 	for (const auto &session : descriptor.sessions) {
 | |
| 		writePostponed(session);
 | |
| 	}
 | |
| 	crl::async([files = std::move(descriptor.files)]{
 | |
| 		for (const auto &file : files) {
 | |
| 			QFile(file.first).remove();
 | |
| 			crl::on_main([descriptor = file.second] {
 | |
| 				if (const auto session = SessionByUniqueId(
 | |
| 						descriptor.sessionUniqueId)) {
 | |
| 					if (const auto id = descriptor.documentId) {
 | |
| 						[[maybe_unused]] const auto location
 | |
| 							= session->data().document(id)->location(true);
 | |
| 					}
 | |
| 					const auto itemId = descriptor.itemId;
 | |
| 					if (const auto item = session->data().message(itemId)) {
 | |
| 						session->data().requestItemRepaint(item);
 | |
| 					}
 | |
| 				}
 | |
| 			});
 | |
| 		}
 | |
| 	});
 | |
| }
 | |
| 
 | |
| bool DownloadManager::loadedHasNonCloudFile() const {
 | |
| 	for (const auto &[session, data] : _sessions) {
 | |
| 		for (const auto &id : data.downloaded) {
 | |
| 			if (const auto object = id.object.get()) {
 | |
| 				if (!object->item->isHistoryEntry()) {
 | |
| 					return true;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| auto DownloadManager::loadingList() const
 | |
| -> ranges::any_view<const DownloadingId*, ranges::category::input> {
 | |
| 	return ranges::views::all(
 | |
| 		_sessions
 | |
| 	) | ranges::views::transform([=](const auto &pair) {
 | |
| 		return ranges::views::all(
 | |
| 			pair.second.downloading
 | |
| 		) | ranges::views::transform([](const DownloadingId &id) {
 | |
| 			return &id;
 | |
| 		});
 | |
| 	}) | ranges::views::join;
 | |
| }
 | |
| 
 | |
| DownloadProgress DownloadManager::loadingProgress() const {
 | |
| 	return _loadingProgress.current();
 | |
| }
 | |
| 
 | |
| rpl::producer<> DownloadManager::loadingListChanges() const {
 | |
| 	return _loadingListChanges.events();
 | |
| }
 | |
| 
 | |
| auto DownloadManager::loadingProgressValue() const
 | |
| -> rpl::producer<DownloadProgress> {
 | |
| 	return _loadingProgress.value();
 | |
| }
 | |
| 
 | |
| bool DownloadManager::loadingInProgress(Main::Session *onlyInSession) const {
 | |
| 	return lookupLoadingItem(onlyInSession) != nullptr;
 | |
| }
 | |
| 
 | |
| HistoryItem *DownloadManager::lookupLoadingItem(
 | |
| 		Main::Session *onlyInSession) const {
 | |
| 	constexpr auto find = [](const SessionData &data) {
 | |
| 		constexpr auto proj = &DownloadingId::done;
 | |
| 		const auto i = ranges::find(data.downloading, false, proj);
 | |
| 		return (i != end(data.downloading)) ? i->object.item.get() : nullptr;
 | |
| 	};
 | |
| 	if (onlyInSession) {
 | |
| 		const auto i = _sessions.find(onlyInSession);
 | |
| 		return (i != end(_sessions)) ? find(i->second) : nullptr;
 | |
| 	} else {
 | |
| 		for (const auto &[session, data] : _sessions) {
 | |
| 			if (const auto result = find(data)) {
 | |
| 				return result;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nullptr;
 | |
| }
 | |
| 
 | |
| void DownloadManager::loadingStopWithConfirmation(
 | |
| 		Fn<void()> callback,
 | |
| 		Main::Session *onlyInSession) {
 | |
| 	const auto window = Core::App().primaryWindow();
 | |
| 	const auto item = lookupLoadingItem(onlyInSession);
 | |
| 	if (!window || !item) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto weak = base::make_weak(&item->history()->session());
 | |
| 	const auto id = item->fullId();
 | |
| 	auto box = Box([=](not_null<Ui::GenericBox*> box) {
 | |
| 		box->addRow(
 | |
| 			object_ptr<Ui::FlatLabel>(
 | |
| 				box.get(),
 | |
| 				tr::lng_download_sure_stop(),
 | |
| 				st::boxLabel),
 | |
| 			st::boxPadding + QMargins(0, 0, 0, st::boxPadding.bottom()));
 | |
| 		box->setStyle(st::defaultBox);
 | |
| 		box->addButton(tr::lng_selected_upload_stop(), [=] {
 | |
| 			box->closeBox();
 | |
| 
 | |
| 			if (!onlyInSession || weak.get()) {
 | |
| 				loadingStop(onlyInSession);
 | |
| 			}
 | |
| 			if (callback) {
 | |
| 				callback();
 | |
| 			}
 | |
| 		}, st::attentionBoxButton);
 | |
| 		box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
 | |
| 		box->addLeftButton(tr::lng_upload_show_file(), [=] {
 | |
| 			box->closeBox();
 | |
| 
 | |
| 			if (const auto strong = weak.get()) {
 | |
| 				if (const auto item = strong->data().message(id)) {
 | |
| 					if (const auto window = strong->tryResolveWindow()) {
 | |
| 						window->showPeerHistoryAtItem(item);
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		});
 | |
| 	});
 | |
| 	window->show(std::move(box));
 | |
| 	window->activate();
 | |
| }
 | |
| 
 | |
| void DownloadManager::loadingStop(Main::Session *onlyInSession) {
 | |
| 	const auto stopInSession = [&](SessionData &data) {
 | |
| 		while (!data.downloading.empty()) {
 | |
| 			cancel(data, data.downloading.end() - 1);
 | |
| 		}
 | |
| 	};
 | |
| 	if (onlyInSession) {
 | |
| 		const auto i = _sessions.find(onlyInSession);
 | |
| 		if (i != end(_sessions)) {
 | |
| 			stopInSession(i->second);
 | |
| 		}
 | |
| 	} else {
 | |
| 		for (auto &[session, data] : _sessions) {
 | |
| 			stopInSession(data);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::clearLoading() {
 | |
| 	Expects(_loading.empty());
 | |
| 
 | |
| 	for (auto &[session, data] : _sessions) {
 | |
| 		while (!data.downloading.empty()) {
 | |
| 			remove(data, data.downloading.end() - 1);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| auto DownloadManager::loadedList()
 | |
| -> ranges::any_view<const DownloadedId*, ranges::category::input> {
 | |
| 	for (auto &[session, data] : _sessions) {
 | |
| 		resolve(session, data);
 | |
| 	}
 | |
| 	return ranges::views::all(
 | |
| 		_sessions
 | |
| 	) | ranges::views::transform([=](const auto &pair) {
 | |
| 		return ranges::views::all(
 | |
| 			pair.second.downloaded
 | |
| 		) | ranges::views::filter([](const DownloadedId &id) {
 | |
| 			return (id.object != nullptr);
 | |
| 		}) | ranges::views::transform([](const DownloadedId &id) {
 | |
| 			return &id;
 | |
| 		});
 | |
| 	}) | ranges::views::join;
 | |
| }
 | |
| 
 | |
| rpl::producer<> DownloadManager::loadedResolveDone() const {
 | |
| 	using namespace rpl::mappers;
 | |
| 	return _loadedResolveDone.value() | rpl::filter(_1) | rpl::to_empty;
 | |
| }
 | |
| 
 | |
| void DownloadManager::resolve(
 | |
| 		not_null<Main::Session*> session,
 | |
| 		SessionData &data) {
 | |
| 	const auto guard = gsl::finally([&] {
 | |
| 		checkFullResolveDone();
 | |
| 	});
 | |
| 	if (data.resolveSentTotal >= data.resolveNeeded
 | |
| 		|| data.resolveSentTotal >= kMaxResolvePerAttempt) {
 | |
| 		return;
 | |
| 	}
 | |
| 	struct Prepared {
 | |
| 		uint64 peerAccessHash = 0;
 | |
| 		QVector<MTPInputMessage> ids;
 | |
| 	};
 | |
| 	auto &owner = session->data();
 | |
| 	auto prepared = base::flat_map<PeerId, Prepared>();
 | |
| 	auto last = begin(data.downloaded);
 | |
| 	auto from = last + (data.resolveNeeded - data.resolveSentTotal);
 | |
| 	for (auto i = from; i != last;) {
 | |
| 		auto &id = *--i;
 | |
| 		const auto msgId = id.itemId.msg;
 | |
| 		const auto info = QFileInfo(id.path);
 | |
| 		if (!info.exists() || info.size() != id.size) {
 | |
| 			// Mark as deleted.
 | |
| 			id.path = QString();
 | |
| 		} else if (!owner.message(id.itemId) && IsServerMsgId(msgId)) {
 | |
| 			const auto groupByPeer = peerIsChannel(id.itemId.peer)
 | |
| 				? id.itemId.peer
 | |
| 				: session->userPeerId();
 | |
| 			auto &perPeer = prepared[groupByPeer];
 | |
| 			if (peerIsChannel(id.itemId.peer) && !perPeer.peerAccessHash) {
 | |
| 				perPeer.peerAccessHash = id.peerAccessHash;
 | |
| 			}
 | |
| 			perPeer.ids.push_back(MTP_inputMessageID(MTP_int(msgId.bare)));
 | |
| 		}
 | |
| 		if (++data.resolveSentTotal >= kMaxResolvePerAttempt) {
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 	const auto check = [=] {
 | |
| 		auto &data = sessionData(session);
 | |
| 		if (!data.resolveSentRequests) {
 | |
| 			resolveRequestsFinished(session, data);
 | |
| 		}
 | |
| 	};
 | |
| 	const auto requestFinished = [=] {
 | |
| 		--sessionData(session).resolveSentRequests;
 | |
| 		check();
 | |
| 	};
 | |
| 	for (auto &[peer, perPeer] : prepared) {
 | |
| 		if (const auto channelId = peerToChannel(peer)) {
 | |
| 			session->api().request(MTPchannels_GetMessages(
 | |
| 				MTP_inputChannel(
 | |
| 					MTP_long(channelId.bare),
 | |
| 					MTP_long(perPeer.peerAccessHash)),
 | |
| 				MTP_vector<MTPInputMessage>(perPeer.ids)
 | |
| 			)).done([=](const MTPmessages_Messages &result) {
 | |
| 				session->data().processExistingMessages(
 | |
| 					session->data().channelLoaded(channelId),
 | |
| 					result);
 | |
| 				requestFinished();
 | |
| 			}).fail(requestFinished).send();
 | |
| 		} else {
 | |
| 			session->api().request(MTPmessages_GetMessages(
 | |
| 				MTP_vector<MTPInputMessage>(perPeer.ids)
 | |
| 			)).done([=](const MTPmessages_Messages &result) {
 | |
| 				session->data().processExistingMessages(nullptr, result);
 | |
| 				requestFinished();
 | |
| 			}).fail(requestFinished).send();
 | |
| 		}
 | |
| 	}
 | |
| 	data.resolveSentRequests += prepared.size();
 | |
| 	check();
 | |
| }
 | |
| 
 | |
| void DownloadManager::resolveRequestsFinished(
 | |
| 		not_null<Main::Session*> session,
 | |
| 		SessionData &data) {
 | |
| 	auto &owner = session->data();
 | |
| 	for (; data.resolveSentTotal > 0; --data.resolveSentTotal) {
 | |
| 		const auto i = begin(data.downloaded) + (--data.resolveNeeded);
 | |
| 		if (i->path.isEmpty()) {
 | |
| 			data.downloaded.erase(i);
 | |
| 			continue;
 | |
| 		}
 | |
| 		const auto item = owner.message(i->itemId);
 | |
| 		const auto media = item ? item->media() : nullptr;
 | |
| 		const auto document = media ? media->document() : nullptr;
 | |
| 		const auto photo = media ? media->photo() : nullptr;
 | |
| 		if (i->download.type == DownloadType::Document
 | |
| 			&& (!document || document->id != i->download.objectId)) {
 | |
| 			generateEntry(session, *i);
 | |
| 		} else if (i->download.type == DownloadType::Photo
 | |
| 			&& (!photo || photo->id != i->download.objectId)) {
 | |
| 			generateEntry(session, *i);
 | |
| 		} else {
 | |
| 			i->object = std::make_unique<DownloadObject>(DownloadObject{
 | |
| 				.item = item,
 | |
| 				.document = document,
 | |
| 				.photo = photo,
 | |
| 			});
 | |
| 			_loaded.emplace(item);
 | |
| 		}
 | |
| 		_loadedAdded.fire(&*i);
 | |
| 	}
 | |
| 	crl::on_main(session, [=] {
 | |
| 		resolve(session, sessionData(session));
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void DownloadManager::checkFullResolveDone() {
 | |
| 	if (_loadedResolveDone.current()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	for (const auto &[session, data] : _sessions) {
 | |
| 		if (data.resolveSentTotal < data.resolveNeeded
 | |
| 			|| data.resolveSentRequests > 0) {
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	_loadedResolveDone = true;
 | |
| }
 | |
| 
 | |
| void DownloadManager::generateEntry(
 | |
| 		not_null<Main::Session*> session,
 | |
| 		DownloadedId &id) {
 | |
| 	Expects(!id.object);
 | |
| 
 | |
| 	const auto info = QFileInfo(id.path);
 | |
| 	const auto document = session->data().document(
 | |
| 		base::RandomValue<DocumentId>(),
 | |
| 		0, // accessHash
 | |
| 		QByteArray(), // fileReference
 | |
| 		TimeId(id.started / 1000),
 | |
| 		QVector<MTPDocumentAttribute>(
 | |
| 			1,
 | |
| 			MTP_documentAttributeFilename(
 | |
| 				MTP_string(info.fileName()))),
 | |
| 		Core::MimeTypeForFile(info).name(),
 | |
| 		InlineImageLocation(), // inlineThumbnail
 | |
| 		ImageWithLocation(), // thumbnail
 | |
| 		ImageWithLocation(), // videoThumbnail
 | |
| 		false, // isPremiumSticker
 | |
| 		0, // dc
 | |
| 		id.size);
 | |
| 	document->setLocation(Core::FileLocation(info));
 | |
| 	_generatedDocuments.emplace(document);
 | |
| 
 | |
| 	id.object = std::make_unique<DownloadObject>(DownloadObject{
 | |
| 		.item = generateFakeItem(document),
 | |
| 		.document = document,
 | |
| 	});
 | |
| 	_loaded.emplace(id.object->item);
 | |
| }
 | |
| 
 | |
| auto DownloadManager::loadedAdded() const
 | |
| -> rpl::producer<not_null<const DownloadedId*>> {
 | |
| 	return _loadedAdded.events();
 | |
| }
 | |
| 
 | |
| auto DownloadManager::loadedRemoved() const
 | |
| -> rpl::producer<not_null<const HistoryItem*>> {
 | |
| 	return _loadedRemoved.events();
 | |
| }
 | |
| 
 | |
| void DownloadManager::remove(
 | |
| 		SessionData &data,
 | |
| 		std::vector<DownloadingId>::iterator i) {
 | |
| 	const auto now = DownloadProgress{
 | |
| 		.ready = _loadingProgress.current().ready - i->ready,
 | |
| 		.total = _loadingProgress.current().total - i->total,
 | |
| 	};
 | |
| 	_loading.remove(i->object.item);
 | |
| 	_loadingDone.remove(i->object.item);
 | |
| 	if (const auto document = i->object.document) {
 | |
| 		_loadingDocuments.remove(document);
 | |
| 	}
 | |
| 	data.downloading.erase(i);
 | |
| 	_loadingListChanges.fire({});
 | |
| 	_loadingProgress = now;
 | |
| 	if (_loading.empty() && !_loadingDone.empty()) {
 | |
| 		_clearLoadingTimer.callOnce(kClearLoadingTimeout);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::cancel(
 | |
| 		SessionData &data,
 | |
| 		std::vector<DownloadingId>::iterator i) {
 | |
| 	const auto object = i->object;
 | |
| 	remove(data, i);
 | |
| 	if (const auto document = object.document) {
 | |
| 		document->cancel();
 | |
| 	} else if (const auto photo = object.photo) {
 | |
| 		photo->cancel();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::changed(not_null<const HistoryItem*> item) {
 | |
| 	if (_loaded.contains(item)) {
 | |
| 		auto &data = sessionData(item);
 | |
| 		const auto i = ranges::find(data.downloaded, item.get(), ByItem);
 | |
| 		Assert(i != end(data.downloaded));
 | |
| 
 | |
| 		const auto media = item->media();
 | |
| 		const auto photo = media ? media->photo() : nullptr;
 | |
| 		const auto document = media ? media->document() : nullptr;
 | |
| 		if (i->object->photo != photo || i->object->document != document) {
 | |
| 			detach(*i);
 | |
| 		}
 | |
| 	}
 | |
| 	if (_loading.contains(item) || _loadingDone.contains(item)) {
 | |
| 		check(item);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::removed(not_null<const HistoryItem*> item) {
 | |
| 	if (_loaded.contains(item)) {
 | |
| 		auto &data = sessionData(item);
 | |
| 		const auto i = ranges::find(data.downloaded, item.get(), ByItem);
 | |
| 		Assert(i != end(data.downloaded));
 | |
| 		detach(*i);
 | |
| 	}
 | |
| 	if (_loading.contains(item) || _loadingDone.contains(item)) {
 | |
| 		auto &data = sessionData(item);
 | |
| 		const auto i = ranges::find(data.downloading, item, ByItem);
 | |
| 		Assert(i != end(data.downloading));
 | |
| 
 | |
| 		// We don't want to download files without messages.
 | |
| 		// For example, there is no way to refresh a file reference for them.
 | |
| 		//entry.object.item = nullptr;
 | |
| 		cancel(data, i);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| not_null<HistoryItem*> DownloadManager::regenerateItem(
 | |
| 		const DownloadObject &previous) {
 | |
| 	return generateItem(previous.item, previous.document, previous.photo);
 | |
| }
 | |
| 
 | |
| not_null<HistoryItem*> DownloadManager::generateFakeItem(
 | |
| 		not_null<DocumentData*> document) {
 | |
| 	return generateItem(nullptr, document, nullptr);
 | |
| }
 | |
| 
 | |
| not_null<HistoryItem*> DownloadManager::generateItem(
 | |
| 		HistoryItem *previousItem,
 | |
| 		DocumentData *document,
 | |
| 		PhotoData *photo) {
 | |
| 	Expects(document || photo);
 | |
| 
 | |
| 	const auto session = document
 | |
| 		? &document->session()
 | |
| 		: &photo->session();
 | |
| 	const auto fromId = previousItem
 | |
| 		? previousItem->from()->id
 | |
| 		: session->userPeerId();
 | |
| 	const auto history = previousItem
 | |
| 		? previousItem->history()
 | |
| 		: session->data().history(session->user());
 | |
| 	const auto flags = MessageFlag::FakeHistoryItem;
 | |
| 	const auto replyTo = MsgId();
 | |
| 	const auto viaBotId = UserId();
 | |
| 	const auto date = base::unixtime::now();
 | |
| 	const auto postAuthor = QString();
 | |
| 	const auto caption = TextWithEntities();
 | |
| 	const auto make = [&](const auto media) {
 | |
| 		return history->makeMessage(
 | |
| 			history->nextNonHistoryEntryId(),
 | |
| 			flags,
 | |
| 			replyTo,
 | |
| 			viaBotId,
 | |
| 			date,
 | |
| 			fromId,
 | |
| 			QString(),
 | |
| 			media,
 | |
| 			caption,
 | |
| 			HistoryMessageMarkupData());
 | |
| 	};
 | |
| 	const auto result = document ? make(document) : make(photo);
 | |
| 	_generated.emplace(result);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| void DownloadManager::detach(DownloadedId &id) {
 | |
| 	Expects(id.object != nullptr);
 | |
| 	Expects(_loaded.contains(id.object->item));
 | |
| 	Expects(!_generated.contains(id.object->item));
 | |
| 
 | |
| 	// Maybe generate new document?
 | |
| 	const auto was = id.object->item;
 | |
| 	const auto now = regenerateItem(*id.object);
 | |
| 	_loaded.remove(was);
 | |
| 	_loaded.emplace(now);
 | |
| 	id.object->item = now;
 | |
| 
 | |
| 	_loadedRemoved.fire_copy(was);
 | |
| 	_loadedAdded.fire_copy(&id);
 | |
| }
 | |
| 
 | |
| DownloadManager::SessionData &DownloadManager::sessionData(
 | |
| 		not_null<Main::Session*> session) {
 | |
| 	const auto i = _sessions.find(session);
 | |
| 	Assert(i != end(_sessions));
 | |
| 	return i->second;
 | |
| }
 | |
| 
 | |
| const DownloadManager::SessionData &DownloadManager::sessionData(
 | |
| 		not_null<Main::Session*> session) const {
 | |
| 	const auto i = _sessions.find(session);
 | |
| 	Assert(i != end(_sessions));
 | |
| 	return i->second;
 | |
| }
 | |
| 
 | |
| DownloadManager::SessionData &DownloadManager::sessionData(
 | |
| 		not_null<const HistoryItem*> item) {
 | |
| 	return sessionData(&item->history()->session());
 | |
| }
 | |
| 
 | |
| DownloadManager::SessionData &DownloadManager::sessionData(
 | |
| 		not_null<DocumentData*> document) {
 | |
| 	return sessionData(&document->session());
 | |
| }
 | |
| 
 | |
| void DownloadManager::writePostponed(not_null<Main::Session*> session) {
 | |
| 	session->account().local().updateDownloads(serializator(session));
 | |
| }
 | |
| 
 | |
| Fn<std::optional<QByteArray>()> DownloadManager::serializator(
 | |
| 		not_null<Main::Session*> session) const {
 | |
| 	return [this, weak = base::make_weak(session.get())]()
 | |
| 		-> std::optional<QByteArray> {
 | |
| 		const auto strong = weak.get();
 | |
| 		if (!strong) {
 | |
| 			return std::nullopt;
 | |
| 		} else if (!_sessions.contains(strong)) {
 | |
| 			return QByteArray();
 | |
| 		}
 | |
| 		auto result = QByteArray();
 | |
| 		const auto &data = sessionData(strong);
 | |
| 		const auto count = data.downloaded.size();
 | |
| 		const auto constant = sizeof(quint64) // download.objectId
 | |
| 			+ sizeof(qint32) // download.type
 | |
| 			+ sizeof(qint64) // started
 | |
| 			+ sizeof(quint32) // size
 | |
| 			+ sizeof(quint64) // itemId.peer
 | |
| 			+ sizeof(qint64) // itemId.msg
 | |
| 			+ sizeof(quint64); // peerAccessHash
 | |
| 		auto size = sizeof(qint32) // count
 | |
| 			+ count * constant;
 | |
| 		for (const auto &id : data.downloaded) {
 | |
| 			size += Serialize::stringSize(id.path);
 | |
| 		}
 | |
| 		result.reserve(size);
 | |
| 
 | |
| 		auto stream = QDataStream(&result, QIODevice::WriteOnly);
 | |
| 		stream.setVersion(QDataStream::Qt_5_1);
 | |
| 		stream << qint32(count);
 | |
| 		for (const auto &id : data.downloaded) {
 | |
| 			stream
 | |
| 				<< quint64(id.download.objectId)
 | |
| 				<< qint32(id.download.type)
 | |
| 				<< qint64(id.started)
 | |
| 				// FileSize: Right now any file size fits 32 bit.
 | |
| 				<< quint32(id.size)
 | |
| 				<< quint64(id.itemId.peer.value)
 | |
| 				<< qint64(id.itemId.msg.bare)
 | |
| 				<< quint64(id.peerAccessHash)
 | |
| 				<< id.path;
 | |
| 		}
 | |
| 		stream.device()->close();
 | |
| 
 | |
| 		return result;
 | |
| 	};
 | |
| }
 | |
| 
 | |
| std::vector<DownloadedId> DownloadManager::deserialize(
 | |
| 		not_null<Main::Session*> session) const {
 | |
| 	const auto serialized = session->account().local().downloadsSerialized();
 | |
| 	if (serialized.isEmpty()) {
 | |
| 		return {};
 | |
| 	}
 | |
| 
 | |
| 	QDataStream stream(serialized);
 | |
| 	stream.setVersion(QDataStream::Qt_5_1);
 | |
| 
 | |
| 	auto count = qint32();
 | |
| 	stream >> count;
 | |
| 	if (stream.status() != QDataStream::Ok || count <= 0 || count > 99'999) {
 | |
| 		return {};
 | |
| 	}
 | |
| 	auto result = std::vector<DownloadedId>();
 | |
| 	result.reserve(count);
 | |
| 	for (auto i = 0; i != count; ++i) {
 | |
| 		auto downloadObjectId = quint64();
 | |
| 		auto uncheckedDownloadType = qint32();
 | |
| 		auto started = qint64();
 | |
| 		// FileSize: Right now any file size fits 32 bit.
 | |
| 		auto size = quint32();
 | |
| 		auto itemIdPeer = quint64();
 | |
| 		auto itemIdMsg = qint64();
 | |
| 		auto peerAccessHash = quint64();
 | |
| 		auto path = QString();
 | |
| 		stream
 | |
| 			>> downloadObjectId
 | |
| 			>> uncheckedDownloadType
 | |
| 			>> started
 | |
| 			>> size
 | |
| 			>> itemIdPeer
 | |
| 			>> itemIdMsg
 | |
| 			>> peerAccessHash
 | |
| 			>> path;
 | |
| 		const auto downloadType = DownloadType(uncheckedDownloadType);
 | |
| 		if (stream.status() != QDataStream::Ok
 | |
| 			|| path.isEmpty()
 | |
| 			|| size <= 0
 | |
| 			|| size > kMaxFileSize
 | |
| 			|| (downloadType != DownloadType::Document
 | |
| 				&& downloadType != DownloadType::Photo)) {
 | |
| 			return {};
 | |
| 		}
 | |
| 		result.push_back({
 | |
| 			.download = {
 | |
| 				.objectId = downloadObjectId,
 | |
| 				.type = downloadType,
 | |
| 			},
 | |
| 			.started = started,
 | |
| 			.path = path,
 | |
| 			.size = int64(size),
 | |
| 			.itemId = { PeerId(itemIdPeer), MsgId(itemIdMsg) },
 | |
| 			.peerAccessHash = peerAccessHash,
 | |
| 		});
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| void DownloadManager::untrack(not_null<Main::Session*> session) {
 | |
| 	const auto i = _sessions.find(session);
 | |
| 	Assert(i != end(_sessions));
 | |
| 
 | |
| 	for (const auto &entry : i->second.downloaded) {
 | |
| 		if (const auto resolved = entry.object.get()) {
 | |
| 			const auto item = resolved->item;
 | |
| 			_loaded.remove(item);
 | |
| 			_generated.remove(item);
 | |
| 			if (const auto document = resolved->document) {
 | |
| 				_generatedDocuments.remove(document);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	while (!i->second.downloading.empty()) {
 | |
| 		remove(i->second, i->second.downloading.end() - 1);
 | |
| 	}
 | |
| 	_sessions.erase(i);
 | |
| }
 | |
| 
 | |
| rpl::producer<Ui::DownloadBarProgress> MakeDownloadBarProgress() {
 | |
| 	return Core::App().downloadManager().loadingProgressValue(
 | |
| 	) | rpl::map([=](const DownloadProgress &progress) {
 | |
| 		return Ui::DownloadBarProgress{
 | |
| 			.ready = progress.ready,
 | |
| 			.total = progress.total,
 | |
| 		};
 | |
| 	});
 | |
| }
 | |
| 
 | |
| rpl::producer<Ui::DownloadBarContent> MakeDownloadBarContent() {
 | |
| 	return [](auto consumer) {
 | |
| 		auto lifetime = rpl::lifetime();
 | |
| 
 | |
| 		struct State {
 | |
| 			DocumentData *document = nullptr;
 | |
| 			std::shared_ptr<Data::DocumentMedia> media;
 | |
| 			rpl::lifetime downloadTaskLifetime;
 | |
| 			QImage thumbnail;
 | |
| 
 | |
| 			base::has_weak_ptr guard;
 | |
| 			bool scheduled = false;
 | |
| 			Fn<void()> push;
 | |
| 		};
 | |
| 
 | |
| 		const auto state = lifetime.make_state<State>();
 | |
| 		auto &manager = Core::App().downloadManager();
 | |
| 
 | |
| 		const auto resolveThumbnailRecursive = [=](auto &&self) -> bool {
 | |
| 			if (state->document
 | |
| 				&& (!state->document->hasThumbnail()
 | |
| 					|| state->document->thumbnailFailed())) {
 | |
| 				state->media = nullptr;
 | |
| 			}
 | |
| 			if (!state->media) {
 | |
| 				state->downloadTaskLifetime.destroy();
 | |
| 				if (!state->thumbnail.isNull()) {
 | |
| 					return false;
 | |
| 				}
 | |
| 				state->thumbnail = QImage();
 | |
| 				return true;
 | |
| 			}
 | |
| 			if (const auto image = state->media->thumbnail()) {
 | |
| 				state->thumbnail = image->original();
 | |
| 				state->downloadTaskLifetime.destroy();
 | |
| 				state->media = nullptr;
 | |
| 				return true;
 | |
| 			} else if (const auto embed = state->media->thumbnailInline()) {
 | |
| 				if (!state->thumbnail.isNull()) {
 | |
| 					return false;
 | |
| 				}
 | |
| 				state->thumbnail = Images::Prepare(embed->original(), 0, {
 | |
| 					.options = Images::Option::Blur,
 | |
| 				});
 | |
| 			}
 | |
| 			state->document->session().downloaderTaskFinished(
 | |
| 			) | rpl::filter([=] {
 | |
| 				return self(self);
 | |
| 			}) | rpl::start_with_next(
 | |
| 				state->push,
 | |
| 				state->downloadTaskLifetime);
 | |
| 			return !state->thumbnail.isNull();
 | |
| 		};
 | |
| 		const auto resolveThumbnail = [=] {
 | |
| 			return resolveThumbnailRecursive(resolveThumbnailRecursive);
 | |
| 		};
 | |
| 
 | |
| 		const auto notify = [=, &manager] {
 | |
| 			auto content = Ui::DownloadBarContent();
 | |
| 			auto single = (const Data::DownloadObject*) nullptr;
 | |
| 			for (const auto id : manager.loadingList()) {
 | |
| 				if (id->hiddenByView) {
 | |
| 					break;
 | |
| 				}
 | |
| 				if (!single) {
 | |
| 					single = &id->object;
 | |
| 				}
 | |
| 				++content.count;
 | |
| 				if (id->done) {
 | |
| 					++content.done;
 | |
| 				}
 | |
| 			}
 | |
| 			if (content.count == 1) {
 | |
| 				const auto document = single->document;
 | |
| 				const auto thumbnailed = (single->item
 | |
| 					&& document->hasThumbnail())
 | |
| 					? document
 | |
| 					: nullptr;
 | |
| 				if (state->document != thumbnailed) {
 | |
| 					state->document = thumbnailed;
 | |
| 					state->media = thumbnailed
 | |
| 						? thumbnailed->createMediaView()
 | |
| 						: nullptr;
 | |
| 					if (const auto raw = state->media.get()) {
 | |
| 						raw->thumbnailWanted(single->item->fullId());
 | |
| 					}
 | |
| 					state->thumbnail = QImage();
 | |
| 					resolveThumbnail();
 | |
| 				}
 | |
| 				content.singleName = Ui::Text::FormatDownloadsName(
 | |
| 					document);
 | |
| 				content.singleThumbnail = state->thumbnail;
 | |
| 			}
 | |
| 			consumer.put_next(std::move(content));
 | |
| 		};
 | |
| 		state->push = [=] {
 | |
| 			if (state->scheduled) {
 | |
| 				return;
 | |
| 			}
 | |
| 			state->scheduled = true;
 | |
| 			Ui::PostponeCall(&state->guard, [=] {
 | |
| 				state->scheduled = false;
 | |
| 				notify();
 | |
| 			});
 | |
| 		};
 | |
| 
 | |
| 		manager.loadingListChanges(
 | |
| 		) | rpl::filter([=] {
 | |
| 			return !state->scheduled;
 | |
| 		}) | rpl::start_with_next(state->push, lifetime);
 | |
| 
 | |
| 		notify();
 | |
| 		return lifetime;
 | |
| 	};
 | |
| }
 | |
| 
 | |
| } // namespace Data
 | 
